tkscene: A scene geometry manager for Tkinter John W. Shipman 2011-09-14 18:56 Abstract A Python-language module that manages the transformations between scene and display geometry for the Tkinter graphical user interface. This publication is available in Web form1 and also as a PDF document2. Please forward any comments to tcc-doc@nmt.edu. Table of Contents 1. Scene coordinates and display coordinates ................................................................................. 3 2. The toolbase ............................................................................................................................ 3 3. Downloadable files .................................................................................................................. 4 4. Design considerations for Tkinter .............................................................................................. 4 5. Space cases .............................................................................................................................. 5 6. Design of the Rudiment class ................................................................................................... 6 7. The tkscene.py file: Prologue ................................................................................................ 7 8. Module imports ....................................................................................................................... 8 9. Manifest constants ................................................................................................................... 8 9.1. Standard angles ............................................................................................................. 8 9.2. COLOR_OPTION ............................................................................................................ 8 9.3. FILL_OPTION .............................................................................................................. 9 9.4. OUTLINE_OPTION ........................................................................................................ 9 9.5. SMOOTH_OPTION .......................................................................................................... 9 9.6. TAGS_OPTION .............................................................................................................. 9 9.7. WIDTH_OPTION ............................................................................................................. 9 9.8. IDENTITY_XFORM ........................................................................................................ 9 10. class Scene: The scene-display transform ............................................................................ 9 10.1. Future work: stacking order ......................................................................................... 11 10.2. Scene.__init__(): Constructor .............................................................................. 11 10.3. Scene.__findTransform(): Map the scene onto the display space ........................... 12 10.4. Scene.__fixAspect(): Match aspect ratios ............................................................. 13 10.5. Scene.__buildTransform(): Build the scene transform .......................................... 13 10.6. Scene.s_d(): Scene to display transform .................................................................. 14 10.7. Scene.d_s(): Display to scene transform ................................................................... 15 10.8. Scene.place(): Add a copy of an element to the scene .............................................. 15 10.9. Scene.remove(): Remove one or all elements ........................................................... 15 10.10. Scene.tkDraw(): Draw the scene on a Tkinter canvas .............................................. 16 10.11. Scene.erase() ...................................................................................................... 16 1 2 http://www.nmt.edu/tcc/help/lang/python/examples/tkscene/ http://www.nmt.edu/tcc/help/lang/python/examples/tkscene/tkscene.pdf New Mexico Tech Computer Center tkscene: A scene geometry manager 1 10.12. Scene.__str__() .................................................................................................. 16 11. class Element: Generic scene element ............................................................................... 17 11.1. Beyond rudiments ...................................................................................................... 18 12. class PlacedElt: One copy of an element in the scene ....................................................... 18 12.1. PlacedElt.__init__() ......................................................................................... 19 12.2. PlacedElt.tkDraw(): Draw this element on a canvas ............................................... 20 12.3. PlacedElt.erase() ............................................................................................... 21 12.4. PlacedElt.__str__() ........................................................................................... 21 13. class Rudiment: Scene display component ......................................................................... 21 13.1. Rudiment class variables ............................................................................................ 23 13.2. Rudiment.__init__(): Constructor ........................................................................ 23 13.3. Rudiment.transform(): Create a transformed rudiment ......................................... 23 13.4. Rudiment.addCan(): Add the oid of a canvas object ................................................. 24 13.5. Rudiment.erase(): Remove this rudiment from the display ..................................... 24 13.6. Rudiment.tkDraw(): Render this rudiment .............................................................. 25 13.7. Rudiment.setWidth(): Store the width value .......................................................... 25 13.8. Rudiment.getWidth(): Set up Tkinter's border width .............................................. 25 13.9. Rudiment.filterOptions(): Set up Tkinter option values ...................................... 26 13.10. Rudiment.__str__() ............................................................................................ 27 14. class StraightRudiment: One line segment .................................................................... 27 14.1. StraightRudiment.__init__() ........................................................................... 28 14.2. StraightRudiment.make(): Factory method .......................................................... 29 14.3. StraightRudiment.tkDraw(): Render it ................................................................ 30 15. class BoxRudiment: Rectangle rudiment ........................................................................... 30 15.1. BoxRudiment.__init__() ..................................................................................... 32 15.2. BoxRudiment.make() ............................................................................................. 32 15.3. BoxRudiment.tkDraw() ......................................................................................... 33 16. class OvalRudiment: Ellipse ............................................................................................ 34 16.1. OvalRudiment.__init__() ................................................................................... 35 16.2. OvalRudiment.make() ........................................................................................... 35 16.3. OvalRudiment.tkDraw() ....................................................................................... 36 16.4. OvalRudiment.__drawOval() ............................................................................... 39 16.5. OvalRudiment.__drawPolygon() ......................................................................... 39 17. class ArcRudiment: Circular arc ....................................................................................... 40 17.1. ArcRudiment.__init__() ...................................................................................... 42 17.2. ArcRudiment.make(): Circular arc .......................................................................... 42 17.3. ArcRudiment.tkDraw() ......................................................................................... 43 18. class PolygonRudiment: Arbitrary polygons .................................................................... 44 18.1. PolygonRudiment.__init__() ............................................................................. 45 18.2. PolygonRudiment.make() ..................................................................................... 45 18.3. PolygonRudiment.tkDraw() ................................................................................. 46 18.4. PolygonRudiment.vertexNo(): Point name for a numbered vertex ......................... 47 19. class Cardinals: A transformable collection of named points ............................................ 47 20. class Box: Basic rectangle .................................................................................................. 48 20.1. Box.__init__(): Constructor .................................................................................. 49 20.2. Box.__contains__() ............................................................................................. 49 20.3. Box.sizes(): Return the sizes of the sides ................................................................ 50 20.4. Box.aspect(): Compute the aspect ratio .................................................................. 50 21. pointsTuple(): Build a Tkinter points tuple ....................................................................... 50 22. An example: conference .................................................................................................... 50 22.1. Prologue .................................................................................................................... 51 22.2. Module imports ......................................................................................................... 52 2 tkscene: A scene geometry manager New Mexico Tech Computer Center 22.3. Manifest constants ...................................................................................................... 22.4. Main program ............................................................................................................ 22.5. class App: The application as a whole ...................................................................... 22.6. App.__createWidgets(): Set up Tkinter widgets .................................................... 22.7. App.__buildScene(): Set up the canvas .................................................................. 22.8. App.__furnish(): Arrange the furniture ................................................................. 22.9. App.__placeTable() ............................................................................................. 22.10. App.__placeCouch() ............................................................................................ 22.11. App.__placeMovingChairs() .............................................................................. 22.12. App.__setChairPositions() ............................................................................. 22.13. App.__angleHandler(): Handler for the chair angle widget ................................... 22.14. class Couch: Sectional seating ............................................................................... 22.15. Couch.__init__() ................................................................................................ 22.16. Couch.render() .................................................................................................... 22.17. class RoundTable ................................................................................................ 22.18. RoundTable.__init__() ...................................................................................... 22.19. RoundTable.render() .......................................................................................... 22.20. Epilogue .................................................................................................................. 52 53 53 54 55 55 56 57 58 59 61 61 63 63 64 64 64 64 1. Scene coordinates and display coordinates In some graphic applications we need to describe items in terms of some other coordinate system than an actual display device. For example, if you are displaying earth maps, you will want to use feet or miles or UTM coordinates; you don't want to worry about exactly where it will show up on your screen. Two vital definitions: scene coordinates The coordinate system that is convenient for the application. display coordinates The addresses of pixels on a display device, or perhaps dots on paper. To address this problem, this document describes the tkscene module, which manages the conversion between scene and display coordinates using the Python programming language3. The initial implementation will use Python's Tkinter graphical user interface4, but other display platforms may be supported in the future. 2. The toolbase This application layer is built on top of a number of other pieces. Here are some relevant documents. • The Python programming language5. • The Tkinter graphical user interface6 for Python. • Graphics transformations with homogeneous coordinates7 describes the homcoord.py module, which takes care of calculations involving general coordinate transforms. 3 http://www.python.org/ http://www.nmt.edu/tcc/help/pubs/tkinter/ 5 http://www.python.org/ 6 http://www.nmt.edu/tcc/help/pubs/tkinter/ 7 http://www.nmt.edu/tcc/help/lang/python/examples/homcoord/ 4 New Mexico Tech Computer Center tkscene: A scene geometry manager 3 3. Downloadable files • tkscene.py8 is the main Python module for this application. • tkscene.xml9 is the DocBook-XML10 source file for this document. • conference11 is a small demonstration script, described in Section 22, “An example: conference” (p. 50). 4. Design considerations for Tkinter This application is built on a 2½-dimensional model: all rendered elements are flat, but they have a stacking order that determines which elements are “in front” of others and therefore visible. We assume that ultimately your scene will be rendered in Tkinter using a Canvas widget. Let us examine the set of graphics primitives available for this widget and consider how each may relate to scene coordinates. Considering the three types of transforms—translation, scaling, and rotation—some primitives cannot be transformed in arbitrary ways. arc In general, a Tkinter arc is a wedge-shaped slice out of an oval. It may be rendered in three styles: PIESLICE, CHORD, and ARC; refer to the Tkinter document for illustrations. For reasons described below under the oval primitive, we can scale and translate arcs but cannot rotate them by arbitrary angles. bitmap A bitmap is a rasterized image using two colors. A bitmap cannot be scaled or rotated, but it can be translated. image A Tkinter image is a rasterized color graphic. Such items cannot be scaled or rotated, but they can be translated. line A Tkinter line is defined by a sequence of two or more points. The rendering can be either a linked sequence of line segments, or an approximated spline curve. These primitives can be translated, scaled, or rotated freely, and will still render correctly in Tkinter. These primitives support a large number of features: dash or dot patterns, arrows, cap and join styles, for instance. The first release of tkscene will support only solid lines of various widths and colors. oval Actually an ellipse, Tkinter ovals are described by the rectangle that encloses them. The ellipses rendered by the Canvas.create_oval() method are always oriented with the major and minor axes parallel to the coordinate axes. However, for ellipses (other than circles) whose bounding boxes are not parallel to the axes, we can use Tkinter's Canvas.create_polygon() method to create a four-vertex polygon with the smooth=1 option to render an ellipse that can be rotated by an arbitrary angle. However, this trick will not work to get rotated arcs. Tkinter's arc rudiments are based on their ellipses, which do not support arbitrary rotation. 8 http://www.nmt.edu/tcc/help/lang/python/examples/tkscene/tkscene.py http://www.nmt.edu/tcc/help/lang/python/examples/tkscene/tkscene.xml http://www.nmt.edu/tcc/help/pubs/docbook/ 11 http://www.nmt.edu/tcc/help/lang/python/examples/tkscene/conference 9 10 4 tkscene: A scene geometry manager New Mexico Tech Computer Center polygon Because polygons are defined by the set of their vertices, they may be translated, scaled, or rotated. rectangle The sides of Tkinter rectangles must be parallel to the major axes, and they are defined by two opposite corner points. However, because Tkinter supports polygons, if we represent rectangles internally using all four corners, we can render rectangles that have been arbitrarily translated, scaled, or rotated. text Tkinter fonts are sized according to pixels or printer's points, and there is no facility for drawing them at any orientation except horizontal. Hence, the only transform they could support is translation. window A Tkinter window item is used to place active widgets on the canvas. They can be positioned and sized explicitly, but their function is tied quite closely to the Tkinter application, and are not really elements of a rendered scene. Hence, tkscene will not support them directly. Here is a table summarizing the Tkinter primitives, and which ones will be supported as tkscene rudiments. You may still use the other primitives, of course, and tkscene provides facilities for positioning them on the canvas in terms of their locations in scene space. Primitive Translate Scale Rotate Rudiment arc Yes Yes No Yes bitmap Yes No No No image Yes No No No line Yes Yes Yes Yes oval Yes Yes Yes Yes polygon Yes Yes Yes No rectangle Yes Yes Yes Yes text Yes No No No window Yes No No No 5. Space cases Complex scenes are often constructed by reproducing many copies of basic building blocks. For example, in a floor plan, you may use a library of furniture items to show the arrangement of tables, desks and chairs. To construct these building blocks, you will draw a diagram showing the rendered elements in some Cartesian coordinate system. Effectively, then, we have to manage three different spaces in our application. • The display space is the final destination, such as a flat panel. Coordinates will be the addresses of specific pixels. • The scene space uses the coordinates that you are modeling, such as miles or centimeters. • Within a graphics building block, the reference space is whatever coordinate system is most convenient for building up a complex image from primitive components. Typically the coordinates of a building block will have the same scale as scene space, so that the process of placing a copy of the building block into the scene consists of translation and rotation transforms. New Mexico Tech Computer Center tkscene: A scene geometry manager 5 Important Rendering order is important. When rendering a compound element whose pieces overlap, it is vital to draw the elements starting from the deepest (background) elements and proceeding toward the viewer, so that those drawn last conceal those drawn earlier, where they overlap. Hence, any item that needs to render itself on the canvas will be a Python generator that generates a sequence of rudiments from back to front. Here, then, is an outline of the rendering process. 1. Write the necessary building blocks you will need to build up your scene. Each building block has a .draw() method that renders itself by generating a sequence of rudiments in its reference space. 2. Create your Tkinter application and the canvas where the rendering will take place. 3. Create a Scene instance that defines the range of scene coordinates and the size of the canvas. This instance can convert scene to display coordinates and back again. 4. Build up your scene using instances of your building blocks. For each copy of a building block placed into the scene, keep track of the transform from the building block's reference space to the scene space. 5. To render the scene, each building block generates a sequence of graphics rudiments in its reference space. As each rudiment is generated, it is transformed into the scene space, and then into the display space, where it is rendered onto the canvas. The various graphics primitives are all derived from base class Rudiment. This class has a .transform(x) method that returns the same primitive transformed into a new space by some transform x (an instance of the homcoord.Xform class). Instances also have a .tkDraw() method that renders that rudiment onto a Tkinter Canvas widget. 6. Design of the Rudiment class There are a number of issues that complicate the implementation of the Rudiment.transform(x) method that takes a transform object x, as a homcoord.Xform instance, and uses it to create a new Rudiment instance in the transformed space. It would be nice to implement the .transform() method in the base class. However, each derived class may have a completely different set of attributes. With this architecture, each derived class would have had to supply its own .transform() method, so that the attributes particular to that class could be transformed into the new instance. As is often the case, the author got into the pool one day to swim laps and emerged with a solution. Ultimately, every graphics rudiment can be defined in terms of a set of parameter values. For example: • A straight line segment is defined by two endpoints. • A circular arc can be defined in several ways, but Tkinter's arcs are defined in terms of a bounding box (which defines the circle) and two angles that specify where along that circle to start and stop drawing. • A rectangle, assuming its sides are parallel to the coordinate axes, is defined by two points at opposite ends of one of its diagonals. So, in order to implement transformation in the base class, it must have some way to find all the points in the instance that must be transformed to the new space. 6 tkscene: A scene geometry manager New Mexico Tech Computer Center Part of the solution is to create a container class called Cardinals that stores all the point coordinates (sometimes called “cardinal points”) that define the shape of the rudiment. Each Rudiment instance has one of these inside it. The Rudiment.transform() method can produce a new Cardinals instance from the old one by applying the transform to each point. There are three remaining problems. • What about attributes of a rudiment that are expressed as angles? For instance, the circular arc primitive in Tkinter is defined by four items: two points that define the bounding box of the circle, and two angles that define which part of the circle is included in the arc. The solution to this is to redesign the rudiments that use angles so that they are defined in terms of points instead. For example, instead of specifying the limits of an arc rudiment as angles, we could use the endpoints of the arc. • What about dimensional attributes? The line rudiment has a width attribute. We want all the dimensions of a rudiment to be expressed in the same space: so, if we are rendering a straight section of rail, we want the width of the rail to be expressed in scene space (feet and inches). The line width should be transformed from reference to scene to display coordinates just like any other. We can't apply a transform to a dimension, but we can do the equivalent graphically. A line from (0, 0) to (0, m) has a length m; so, if we transform both those points, the distance between them afterward is the transformed dimension. In practice, rather than defining two points, we'll reuse one of the other cardinal points (x, y), and define a width reference point at (x+m, y). When it comes time to render the line, we measure the distance between those points to get the current width. • What about non-dimensional attributes like colors? A color attribute is specific to the derived class, not to rudiments generally. One approach is to carry these attributes in a separate dictionary rather than make them part of the Cardinals class. There is a more subtle problem. How does the Rudiment.transform() method create the new instance? The .__class__ attribute of the derived class tells us what class constructor to call. But how does a method in the base class know what the argument list of the constructor of the derived class looks like? The solution to all these problems is to carry all the effective attributes of a Rudiment instance in only two physical attributes: • The points that define the shape of the rudiment are stored as named points in its Cardinals instance. Linear dimensions are represented as pairs of points. • Non-dimensional attributes are carried in a separate dictionary; we'll call it the aux (for auxiliary) dictionary. Now we can restrict the constructor calling sequences of all the derived classes to this general form: Rudiment ( tagList, cardinals, aux ) So the Rudiment.transform() method knows what constructor to call—self.__class__()— and what arguments to pass it. 7. The tkscene.py file: Prologue The tkscene module starts with a module documentation string that points back at this documentation. New Mexico Tech Computer Center tkscene: A scene geometry manager 7 tkscene.py '''tkscene.py: Tkinter scene transform manager. Do not edit this file. It is extracted automatically from the documentation: http://www.nmt.edu/tcc/help/lang/python/examples/tkscene/ ''' 8. Module imports All the coordinate transform geometry is handled by the homcoord package; see Graphics transformations with homogeneous coordinates12. This module includes pi and all the standard trig functions, although some of the function names differ from those in the standard Python math module: arctan2 and arccos instead of atan2 and acos. tkscene.py # - - - - - I m p o r t s from homcoord import * The current graphics base is Tkinter. tkscene.py from Tkinter import * 9. Manifest constants This section declares names that are constant and global to the application. tkscene.py # - - - - - M a n i f e s t c o n s t a n t s 9.1. Standard angles The four cardinal directions as Cartesian angles in radians, and other common angles. Constants RAD_90 and RAD_180, representing 90° and 180° in radians, come from the homcoord module. tkscene.py ANGLE_E ANGLE_N ANGLE_W ANGLE_S = = = = 0.0 RAD_90 RAD_180 RAD_180 + RAD_90 9.2. COLOR_OPTION Name of the keyword option used to specify the color of a line. tkscene.py COLOR_OPTION = 'color' 12 8 http://www.nmt.edu/tcc/help/lang/python/examples/homcoord/ tkscene: A scene geometry manager New Mexico Tech Computer Center 9.3. FILL_OPTION Name of the keyword option used to specify the interior color of a box. tkscene.py FILL_OPTION = 'fill' 9.4. OUTLINE_OPTION Name of the keyword option for the color of the outline of a box. tkscene.py OUTLINE_OPTION = 'outline' 9.5. SMOOTH_OPTION This is an option sent to Tkinter, not an option for our exported interface. tkscene.py SMOOTH_OPTION = 'smooth' 9.6. TAGS_OPTION This is an option sent to Tkinter, not an option for our exported interface. tkscene.py TAGS_OPTION = 'tags' 9.7. WIDTH_OPTION Keyword option for specifying line and border widths. tkscene.py WIDTH_OPTION = 'width' 9.8. IDENTITY_XFORM An instance of homcoord.Xform that leaves coordinates untouched. It is implemented as a translation with zero offsets. tkscene.py IDENTITY_XFORM = Xlate( (0.0, 0.0) ) 10. class Scene: The scene-display transform The concern of this class is the relationship between scene coordinates and display coordinates. It is also a container for PlacedElt instances that describe the scene. tkscene.py # - - - - - c l a s s New Mexico Tech Computer Center S c e n e tkscene: A scene geometry manager 9 class Scene(object): '''Describes the scene-display relationship. Exports: Scene(sceneBounds, displayBounds, mag=1.0): [ (sceneBounds is a Box instance specifying the range of scene coordinates in Cartesian form) and (displayBounds is a Box instance specifying the range of display coordinates with +x to the right and +y down) and (mag is the ratio between the virtual display bounds and the display viewport, such that mag=1.0 means that the scene will always fit within the displayBounds) -> return a new Scene instance that can transform scene coordinates to and from display coordinates ] .sceneBounds: [ as passed to constructor, read-only ] .displayBounds: [ as passed to constructor, read-only ] .mag: [ as passed to constructor, read-only ] .toDisplay: [ the scene-to-display coordinate transform as a homcoord.Xform instance ] .fromDisplay: [ the display-toa-scene coordinate transform as a homcoord.Xform instance ] .s_d(s): [ s is a point in scene space as a homcoord.Pt instance -> return the corresponding display coordinates as a homcoord.Pt instance ] .d_s(d): [ d is a point in display space as a homcoord.Pt instance -> return the corresponding scene coordinates as a homcoord.Pt instance ] .place(pelt): [ (pelt is a PlacedElement) -> self := self with a copy of pelt added at the end of self's drawing sequence return pelt ] .remove ( pelt=None ): [ if pelt is None -> self := self with all elements removed ] else if pelt is a PlacedElt in self's drawing sequence -> self := self with pelt removed else -> I ] .tkDraw(can): [ can is a Tkinter.Canvas widget -> can := can - (any previous rendering of self) + (self, rendered) ] .erase(): [ if the scene is currently rendered on a canvas -> remove all scene's elements from that canvas else -> I ] .__str__(): [ return a string representation of self ] The list of placed elements in the scene is a private attribute. 10 tkscene: A scene geometry manager New Mexico Tech Computer Center tkscene.py State/Invariants: .__drawList: [ a sequence of PlacedElt instances representing the contents of self's scene ] ''' 10.1. Future work: stacking order In this version, elements are always added at the end of .__drawList. The only way to change the stacking order is to remove an element and then place it again, and in that case it is always on top. There are lots of possible ways to give the application control over the stacking order. Here is the author's current thinking on how to implement this. • The application would define a sequence of layers, from back to front. Each layer would be identified by some name. • When an element is added to the scene, it can specify to which layer it belongs. (It might be useful to have a default, nameless layer containing elements that do not specify a layer name. This might make for less work in converting legacy applications.) • The Scene class would manage the canvas's stacking order so that whenever a rudiment is rendered onto the canvas it is tagged with the layer name. • Whenever an element is added to a layer other than the top layer, its component display rudiments are restacked by calling the Tkinter Canvas.tag_lower(b) method on them; this method inserts the canvas items just before b in its stacking order. The value of b may be either a tag or an object ID. • If it is a tag, there must be at least one item that carries that tag. Hence, the Scene instance must know the tag of the next higher layer that actually has items in it. If there are no such items, the new rudiments can correctly be rendered at the top of the stack. • If the Scene instance knows the OIDs of all the items it is managing, it can use the OID of the first item in the drawing sequence that is above the layer where the new element is to be rendered. For example, suppose you decide to enhance the furniture-arranging application described in Section 22, “An example: conference” (p. 50). You want to implement seagulls that fly around and, because this is a plan view, they always appear above (in front of) the furniture and the stage. This example could be managed with three layers. In order from back to front, you might assign them names 'room', 'furniture', and 'birds'. When a piece of furniture is moved, it will be necessary for the Scene class to insure that all the associated rudiments are placed under all bird-layer rudiments in the canvas's stacking order, so that the furniture will appear behind the birds. 10.2. Scene.__init__(): Constructor tkscene.py # - - - S c e n e . _ _ i n i t _ _ def __init__(self, sceneBounds, displayBounds, mag=1.0): '''Constructor. ''' #-- 1 -self.sceneBounds = sceneBounds New Mexico Tech Computer Center tkscene: A scene geometry manager 11 self.displayBounds = displayBounds self.mag = mag self.__drawList = [] For the method that determines the scene-display transform, see Section 10.3, “Scene.__findTransform(): Map the scene onto the display space” (p. 12). tkscene.py #-- 2 -# [ self.toDisplay := a transform that converts a scene # coordinate into a display coordinate # self.fromDisplay := inverse of that transform ] self.__findTransform() 10.3. Scene.__findTransform(): Map the scene onto the display space tkscene.py # - - - S c e n e . _ _ f i n d T r a n s f o r m def __findTransform(self): '''Figure out how to fit the scene into the display space. [ self.sceneBounds, self.displayBounds, and self.mag are as invariant -> self.toDisplay := a transform that converts a scene coordinate into a display coordinate ] self.fromDisplay := inverse of that transform ] ''' The purpose of this method is to compute a single Xform instance that represents the transform from a point in the scene space to the coordinates of that point in the display space. This transform can be used in the opposite direction, converting the coordinate of a mouse click on the display into the equivalent scene coordinates. There is no guarantee that the display rectangle is the same shape as the scene rectangle, so the first step is to modify the display rectangle so it has the same aspect ratio, defined as the ratio of the width to the height. For this logic, see Section 10.4, “Scene.__fixAspect(): Match aspect ratios” (p. 13). tkscene.py #-- 1 -# [ self.displayBounds := self.displayBounds reduced if # necessary so that it has the same aspect ratio as # self.sceneBounds ] self.__fixAspect() For the logic that assembles the transform from basic operations, “Scene.__buildTransform(): Build the scene transform” (p. 13). see Section 10.5, tkscene.py #-- 2 -# [ self.toDisplay := a transform that converts a scene # coordinate to a coordinate in self.displayBounds # self.fromDisplay := inverse of that transform ] self.__buildTransform() 12 tkscene: A scene geometry manager New Mexico Tech Computer Center 10.4. Scene.__fixAspect(): Match aspect ratios tkscene.py # - - - S c e n e . _ _ f i x A s p e c t def __fixAspect(self): '''Make the display bounds the same shape as the scene bounds. ''' The aspect ratio of a box, the ratio of its height to its width, is a larger number for wider boxes, smaller for narrower boxes. First compute the aspect ratios of the scene and display bounds. tkscene.py #-- 1 -# [ aScene := aspect ratio of self.sceneBounds # aDisplay := aspect ratio of self.displayBounds ] aScene = self.sceneBounds.aspect() aDisplay = self.displayBounds.aspect() If the display's aspect ratio is smaller that the scene's, the display is too tall; reduce the height. Otherwise, the display is too wide; reduce the width. tkscene.py #-- 2 -# [ if aDisplay < aScene -> # self.displayBounds := self.displayBounds # height reduced to its width divided by # else -> # self.displayBounds := self.displayBounds # width reduced to its height multiplied w, h = self.displayBounds.sizes() if aDisplay < aScene: h = w / aScene elif aDisplay > aScene: w = h * aScene self.displayBounds = Box ( Pt(0, 0), Pt(w, h) ) with its aScene with its by aScene ] 10.5. Scene.__buildTransform(): Build the scene transform tkscene.py # - - - S c e n e . _ _ b u i l d T r a n s f o r m def __buildTransform(self): '''Construct the scene-display transform. [ self.toDisplay := a transform that converts a scene coordinate to a coordinate in self.displayBounds self.fromDisplay := inverse of that transform ] ''' To build the transform that takes us from scene space to display space, we will use the composition of a number of the basic transforms from the homcoord module. The x domain of scene coordinates is the range [xmin, xmax], and the y domain is [ymin, ymax]. The first transform removes the minimum values so that both start at zero. New Mexico Tech Computer Center tkscene: A scene geometry manager 13 tkscene.py #-- 1 -# [ t1 := an Xform that translates scene coordinates to # the origin ] xMin, yMin = self.sceneBounds.cMin.xy() t1 = Xlate ( (-xMin, -yMin) ) Next, we compose a transform that normalizes the maximum coordinates, so the x and y values are now in the interval [0, 1]. tkscene.py #-- 2 -# [ t2 := t1 composed with an Xform such that x' is x # divided by xMax - xMin and y= is y divided by # yMax - yMin ] w, h = self.sceneBounds.sizes() t2 = t1.compose ( Xscale ( (1.0/w, 1.0/h) ) ) The next two transforms are necessary because display coordinates are vertically flipped from scene coordinates: in the display, (0, 0) is the upper left corner, not the lower left corner as in Cartesian coordinates. The first of these transforms inverts the y coordinate, and the second translates the y coordinate so that the new y range is [1,0]. tkscene.py #-- 3 -# [ t3 := t2 composed with an Xform that changes the sign # of the y coordinate ] t3 = t2.compose ( Xscale ( (1.0, -1.0) ) ) #-- 4 -# [ t4 := t3 composed with an Xform that translates y # coordinates by +1 ] t4 = t3.compose ( Xlate ( (0.0, 1.0) ) ) At this point, the x range is [0, 1] and the y range is [1, 0]. The next transform scales the coordinates to the display space. tkscene.py #-- 5 -# [ self.toDisplay := t4 composed with an Xform that # scales coordinates to self.displayBounds # self.fromDisplay := inverse of that transform ] wd, hd = self.displayBounds.cMax.xy() self.toDisplay = t4.compose ( Xscale ( (wd, hd) ) ) self.fromDisplay = self.toDisplay.inverse() 10.6. Scene.s_d(): Scene to display transform tkscene.py # - - - S c e n e . s _ d def s_d ( self, s ): '''Convert scene to display coordinates. ''' return self.toDisplay.apply(s) 14 tkscene: A scene geometry manager New Mexico Tech Computer Center 10.7. Scene.d_s(): Display to scene transform tkscene.py # - - - S c e n e . d _ s def d_s ( self, d ): '''Convert display to scene coordinates. ''' return self.toDisplay.invert(d) 10.8. Scene.place(): Add a copy of an element to the scene This method adds a copy of some element to the scene. tkscene.py # - - - S c e n e . p l a c e def place ( self, pelt ): '''Add a PlacedElt to the end of the drawing list. ''' #-- 1 -self.__drawList.append ( pelt ) #-- 2 -return pelt 10.9. Scene.remove(): Remove one or all elements Erases the given PlacedElt from the scene and removes it from the display list. Default is to erase and remove everything. tkscene.py # - - - S c e n e . r e m o v e def remove ( self, pelt=None ): '''Remove one or all elements. ''' #-- 1 -# [ if pelt is None -> # all elements of self.__drawList := # erased if rendered # self.__drawList := an empty list # return # else -> I ] if pelt is None: for elt in self.__drawList: elt.erase() self.__drawList = [] return those elements #-- 2 -# [ if pelt is in self.__drawList -> # pelt := pelt erased if rendered New Mexico Tech Computer Center tkscene: A scene geometry manager 15 # self.__drawList := self.__drawList with pelt removed # else -> I ] try: pelt.erase() self.__drawList.remove(pelt) except ValueError: pass 10.10. Scene.tkDraw(): Draw the scene on a Tkinter canvas Renders all the placed elements in the drawing list. See Section 12, “class PlacedElt: One copy of an element in the scene” (p. 18) for its .tkDraw() method. tkscene.py # - - - S c e n e . t k D r a w def tkDraw ( self, can ): '''Render self on a canvas. ''' #-- 1 -# [ if self is currently rendered on a canvas -> # remove from that canvas all rendered rudiments # else -> I ] self.erase() #-- 2 -# [ can := can with self rendered onto it ] for pelt in self.__drawList: pelt.tkDraw ( can ) 10.11. Scene.erase() The internal attribute self.__drawList enumerates the PlacedElt instances in the scene. For the .erase() method of this class, see Section 12, “class PlacedElt: One copy of an element in the scene” (p. 18). tkscene.py # - - - S c e n e . e r a s e def erase ( self ): '''Erase the entire scene (but retain the drawing list). ''' #-- 1 -for pelt in self.__drawList: pelt.erase() 10.12. Scene.__str__() This method returns a debug display of the entire scene. tkscene.py # - - - 16 S c e n e . _ _ s t r _ _ tkscene: A scene geometry manager New Mexico Tech Computer Center def __str__(self): '''Return a string display of self. ''' return "\n".join ( [ str(pelt) for pelt in self.__drawList ] ) 11. class Element: Generic scene element This is a base class for prefabricated pieces of your scene. For instance, supposed you are writing an application to set up floor plans. If you have to show how 100 identical chairs can be set up in an auditiorium, you shouldn't have to describe the coordinates of every vertex of every line or curve segment individually. Instead, you need only produce a scale drawing of one chair, and work out other details: how closely should chairs be spaced in a row? how much room should there be between rows? Each different furniture type is then represented as a subclass of Element. To write such a class, follow this procedure. 1. On a scale drawing of the piece of furniture, define a Cartesian coordinate system by selecting an origin, an x axis, and a y axis. 2. Figure out how to draw the figure using only the available rudiment types. This is not always easy; see Section 11.1, “Beyond rudiments” (p. 18) for some help in the harder cases. 3. Assign Cartesian coordinates to points that define the rudiments. 4. Write a .render(C) method that generates rudiments in z-coordinate order, back to front, where C is a Tkinter.Canvas widget onto which the rudiments are rendered. Here is the interface to the class. tkscene.py # - - - - - c l a s s E l e m e n t class Element(object): '''Base class for scene elements. Exports: Element(): [ return a new, empty Element instance ] .render(): [ generate a sequence of Rudiment instances that renders self onto can, back to front, in the element's reference space ] .__str__(): [ return a string display of self ] ''' def __init__(self): pass def render(self): raise NotImplementedError("You must override the " "Element.render() method in the derived class." ) New Mexico Tech Computer Center tkscene: A scene geometry manager 17 def __str__(self): return ( "<Element>\n%s" % '\n'.join ( [ str(rudi) for rudi in self.render() ] ) ) 11.1. Beyond rudiments This system is not intended to be a full-featured graphics rendering engine. Many common graphics features such as texture mapping are not supported. When you are writing your Element subclass and find that some aspect of the element is hard to render with the existing set of rudiments, in this package you have basically two choices. • Invent new kinds of rudiments that can still be translated to primitive display elements. • Draw the elements directly onto the canvas, using the Scene instance to figure out how a given scene location translates to a location on the display. As an example of the latter approach, suppose you are rendering a coffee table in plan view, and you would like to display wood grain on its surface. It could be implemented like this. 1. Devise an image file containing the wood grain that looks acceptable at 1:1 rendering into display coordinates. Make it oversize, or use an image that can be tiled to make arbitrarily large areas. 2. Figure out the display coordinates of the coffee table. 3. Use the Python Imaging Library13 to crop the original image to the current display coordinates, and display it directly on the canvas using .create_image(). 12. class PlacedElt: One copy of an element in the scene An instance of this class represents an Element instance that has been placed in the scene. The placement is represented as a homcoord.Xform instance that can transform a point in the element's reference space to the same point in the scene. To move an existing placed element, erase it, modify its .xform attribute to reflect the new position, and redraw it. tkscene.py # - - - - - c l a s s P l a c e d E l t class PlacedElt(object): '''Represents a copy of an Element placed into the scene. Exports: PlacedElt ( scene, element, xform=None ): [ (scene is the containing Scene instance) and (element is an Element instance) and (xform is a homcoord.Xform instance that describes the transform from the element's reference space to scene space) -> scene := scene with a new copy of (element) placed 13 http://www.nmt.edu/tcc/help/pubs/pil/ 18 tkscene: A scene geometry manager New Mexico Tech Computer Center into the scene according to (xform) return a new PlacedElt instance representing those values ] .element: [ as passed to constructor ] .xform: [ as passed, with defaulting, read/write ] .tkDraw(can): [ can is a Tkinter.Canvas widget -> can := can - (any previous rendering of self) + (self, rendered) ] .erase(): [ if self is currently rendered on a canvas -> remove from that canvas all rendered rudiments else -> I ] .__str__(): [ return a string representation of self ] When we render the element as a stream of rudiments, we must retain a list of those rudiments, so they can be erased later. We also remember the canvas so the caller of .erase() doesn't need to provide it. tkscene.py State/Invariants: .__rudiList: [ if self has ever been rendered -> a list containing the Rudiment instances in display space generated during rendering else -> an empty list ] .__can: [ if self is rendered on a canvas -> that Tkinter.Canvas else -> None ] ''' 12.1. PlacedElt.__init__() tkscene.py # - - - P l a c e d E l t . _ _ i n i t _ _ def __init__(self, scene, element, xform=None): '''Constructor. ''' #-- 1 -self.scene = scene self.element = element self.xform = xform or IDENTITY_XFORM self.__rudiList = [] self.__can = None #-- 2 -# [ scene := scene with self added at the end of its # display list ] self.scene.place(self) New Mexico Tech Computer Center tkscene: A scene geometry manager 19 12.2. PlacedElt.tkDraw(): Draw this element on a canvas The entire rendering pipeline is in this method. 1. The original element generates a sequence of rudiments in its reference space. 2. We use self.xform to transform each rudiment into the scene space. 3. We use the parent scene's .toDisplay transform to transform each scene space rudiment into a display space rudiment. 4. The display space rudiment is drawn onto the canvas. We also retain a reference to each rendered display rudiment in self.__rudiList so we can erase them when the .erase() method is called. tkscene.py # - - - P l a c e d E l t . t k D r a w def tkDraw(self, can): '''Render self onto a canvas. ''' #-- 1 -# [ if self is currently rendered onto a canvas -> # remove its rendering from that canvas # else -> I ] self.erase() #-- 2 -self.__can = can #-# [ # # # for 3 -can := can with self's element rendered onto it, transformed by self.xform and then by scene.toDisplay self.__rudiList := display Rudiment instances used to render, in drawing order ] eltRudi in self.element.render(): #-- 3 body -# [ eltRudi is a Rudiment in self.element's reference # space -> # can := can with eltRudi rendered onto it, # transformed by self.xform and then by # scene.toDisplay # self.__rudiList +:= eltRudi, transformed by # self.xform and then by scene.toDisplay ] #-- 3.1 -# [ sceneRudi := eltRudi, transformed by self.xform ] sceneRudi = eltRudi.transform ( self.xform ) #-- 3.2 -# [ displayRudi := sceneRudi, transformed by # scene.toDisplay # self.__rudiList +:= (same) ] displayRudi = sceneRudi.transform ( self.scene.toDisplay ) self.__rudiList.append ( displayRudi ) #-- 3.3 -- 20 tkscene: A scene geometry manager New Mexico Tech Computer Center # [ can := can with displayRudiment rendered onto it ] displayRudi.tkDraw ( can ) 12.3. PlacedElt.erase() When the .tkDraw() method of a display-space Rudiment instance has been called to render it onto a canvas, that instance can also erase itself. We keep a list of those rudiments in self.__rudiList. tkscene.py # - - - P l a c e d E l t . e r a s e def erase ( self ): '''Remove self's rendering from its canvas. ''' #-- 1 -if self.__can is None: return #-- 2 -# [ self.__can := self.__can - (rendering of rudiments in # self.__rudiList) ] for rudi in self.__rudiList: rudi.erase ( self.__can ) #-- 3 -self.__rudiList = [] self.__can = None 12.4. PlacedElt.__str__() Returns a debug display of the contents of self's element and transform. tkscene.py # - - - P l a c e d E l t . _ _ s t r _ _ def __str__(self): '''Return a string display of self. ''' return ( "<PElt(%s)>\n%s)>" % (self.xform, self.element) ) 13. class Rudiment: Scene display component When a scene object is rendered onto the display, it is rendered as a sequence of instances of this class. Each instance corresponds to one of the display elements of Tkinter's Canvas widget: straight lines, splines, rectangles, and so forth. See Section 6, “Design of the Rudiment class” (p. 6) for a lengthy discussion of the arcana required for the .transform() method, and why points are kept inside a Cardinals instance. tkscene.py # - - - - - c l a s s New Mexico Tech Computer Center R u d i m e n t tkscene: A scene geometry manager 21 class Rudiment(object): '''Base clase for displayed scene elements. Exports: Rudiment(tagList, cardinals, **kw): [ (tagList is a sequence of tags to be applied to display elements for this rudiment) and (cardinals is a set of named points as a Cardinals instance) and (kw is a dictionary of attributes specific to the derived class) -> return a new Rudiment instance representing those values ] .tagList: [ as constructor ] .p: [ the cardinals argument passed to the constructor ] .aux: [ the kw argument passed to the constructor ] .transform(x): [ x is a transform as a homcoord.Xform instance -> return a new instance of self's class with its points transformed by x and kw==self.aux ] .addCan(oid): [ oid is an object ID on a canvas -> self := self with that oid added ] .erase(can): [ can is a Tkinter canvas -> can := can with all display elements pertaining to self removed ] .tkDraw(can): # Virtual method [ can is a Tkinter canvas -> can := can - (display elements pertaining to self) + (display elements rendering self) ] Rudiment.setWidth(c, ref, kw): [ (c is a Cardinals instance) and (ref is a homcoord.Pt instance) and (kw is a dictionary) -> if kw has a WIDTH_OPTION key -> c := c with a point named Rudiment.PT_W defined at a distance kw[WIDTH_OPTION] from ref in the +x direction else -> I ] .getWidth(kw, ref): [ (kw is a dictionary) and (ref is a homcoord.Pt) -> if self has a point named Rudiment.PT_W -> kw[WIDTH_OPTION] := the distance from ref to that point, but not less than 1 ] Rudiment.filterOptions(tagList, kw, validOptions): [ (tagList is as in the constructor) and (kw is the user's option dictionary) and (validOptions is a dictionary of valid option keys and their default values or None if there is no default) -> return a new dictionary that is a copy of validOptions 22 tkscene: A scene geometry manager New Mexico Tech Computer Center but updated by any entries in kw whose keys are in validOptions, plus an entry with key 'tags' and value (tagList) ] .__str__(self): [ display self as a string ] State/Invariants: .__oidList: [ list of canvas object IDs rendering self ] ''' 13.1. Rudiment class variables Several derived classes include a width attribute that describes the width of the outline in scene dimensions. By convention, class constant PT_W is the name of a point whose distance from some reference point defines the width. This convention allows use of two base class methods: Section 13.7, “Rudiment.setWidth(): Store the width value” (p. 25) to check for a width attribute and define point PT_W, and Section 13.8, “Rudiment.getWidth(): Set up Tkinter's border width” (p. 25) to scale the width, if present, into a display option. tkscene.py PT_W = 'W' 13.2. Rudiment.__init__(): Constructor tkscene.py # - - - R u d i m e n t . _ _ i n i t _ _ def __init__(self, tagList, cardinals, **kw): '''Constructor ''' self.tagList = tagList self.p = cardinals self.aux = kw self.__oidList = [] 13.3. Rudiment.transform(): Create a transformed rudiment Be sure to read Section 6, “Design of the Rudiment class” (p. 6) if you want to understand why this code requires so much Python magic. tkscene.py # - - - R u d i m e n t . t r a n s f o r m def transform(self, x): '''Return a new Rudiment transformed using x. ''' The new instance is basically a copy of self except that the Cardinals instance, self.p, has all its points transformed using x, which is an instance of homcoord.Xform. tkscene.py #-- 1 -# [ newCardinals := self.p transformed by x ] newCardinals = self.p.transform(x) New Mexico Tech Computer Center tkscene: A scene geometry manager 23 Warning The following code works only if every derived class has exactly the same constructor calling sequence as the base class. We have everything we need now to create the new instance of self's class. What follows demonstrates, in the author's humble opinion, something that might be hard to do in another language. The problem is that this method has to work for all derived classes, so it has to create an instance of that same derived class. This calls for understanding of two Python special class attributes: the .__new(), used to construct a new, uninitialized instance; and .__class__, which is the class object for any value. tkscene.py #-- 2 -# [ newRudi := an unitialized instance of self's class ] newRudi = self.__class__.__new__(self.__class__) #-- 3 -# [ newRudi := newRudi initialized with taglist=self.tagList, # cardinals=newCardinals, and kw=a copy of self.aux ] self.__class__.__init__(newRudi, self.tagList, newCardinals, **self.aux) #-- 4 -return newRudi In the constructor call above, it is important to note that when you pass a pre-build dictionary to a function's keyword arguments using the f(**d) syntax, the called function receives a copy of your dictionary, so you can be sure that that function will not modify your dictionary. 13.4. Rudiment.addCan(): Add the oid of a canvas object tkscene.py # - - - R u d i m e n t . a d d C a n def addCan ( self, oid ): '''Keep track of a canvas object ID so we can delete it later. ''' self.__oidList.append ( oid ) return oid 13.5. Rudiment.erase(): Remove this rudiment from the display tkscene.py # - - - R u d i m e n t . e r a s e def erase(self, can): '''Remove self's elements from the canvas. ''' #-- 1 -# [ can := can minus elements whose oids are in self.__oidList ] for oid in self.__oidList: 24 tkscene: A scene geometry manager New Mexico Tech Computer Center can.delete(oid) #-- 2 -self.__oidList = [] 13.6. Rudiment.tkDraw(): Render this rudiment tkscene.py # - - - R u d i m e n t . t k D r a w def tkDraw(self, can): '''Virtual method. ''' raise NotImplementedError ( "Rudiment.tkDraw() is a " "virtual method.") 13.7. Rudiment.setWidth(): Store the width value This is a service routine to be used by the .make() method of derived classes. The purpose of this static method is to check for a WIDTH option in a derived class's .make() method and, if one is found, to store a point named Rudiment.PT_W whose distance from some reference point defines the width. For a discussion of why widths are stored as points, see Section 6, “Design of the Rudiment class” (p. 6). tkscene.py # - - - R u d i m e n t . s e t W i d t h @staticmethod def setWidth ( c, ref, kw ): '''Set the WIDTH_OPTION if specified. ''' #-- 1 -# [ if kw has a key WIDTH_OPTION -> # w := the corresponding value # else -> return ] try: w = kw[WIDTH_OPTION] except KeyError: return #-- 2 -# [ c := c with a point named Rudiment.PT_W located # (w) away from (ref) in the +x direction ] c[Rudiment.PT_W] = Pt ( ref.x()+w, ref.y() ) 13.8. Rudiment.getWidth(): Set up Tkinter's border width This is a service routine to be used by the .tkDraw() method of derived classes. New Mexico Tech Computer Center tkscene: A scene geometry manager 25 The purpose is to check for a user-specified border width. If it was specified, the instance will have a point named Rudiment.PT_W whose distance from the given ref point defines the border width. If present, this point is translated into a width value for Tkinter. tkscene.py # - - - R u d i m e n t . g e t W i d t h def getWidth ( self, kw, ref ): '''Check for a user-specified border width. ''' #-- 1 -# [ if self has a point named Rudiment.PT_W -> # w := the distance from that point to (ref) # else -> return ] try: w = ref.dist(self.p[self.PT_W]) except KeyError: return #-- 2 -# [ kw[WIDTH_OPTION] := w rounded to an integer, but not # less than 1 ] kw[WIDTH_OPTION] = max ( 1, int(round(w)) ) 13.9. Rudiment.filterOptions(): Set up Tkinter option values This static method is for use by the .make() method of derived classes. It builds the .aux dictionary of options. • The validOptions argument is a dictionary whose set of keys defines the expected option names. The related value for each key is the default option value; if there is no default, specify None as the related value. • For any entry in the user-specified kw dictionary whose key is also a key in validOptions, the keyvalue pair from kw will be in the result. • A key-value pair will be added with key 'tags' and value tagList, in which form it will be passed to the Canvas item constructor. tkscene.py # - - - R u d i m e n t . f i l t e r O p t i o n s @staticmethod def filterOptions(tagList, kw, validOptions): '''Set up an option dictionary for Tkinter. ''' #-- 1-# [ aux := a new dictionary containing all the key-value # pairs from (validOptions) whose values are not None ] aux = {} for key, value in validOptions.items(): if value is not None: aux[key] = value #-- 2 -- 26 tkscene: A scene geometry manager New Mexico Tech Computer Center # [ aux := aux with any entries whose keys are in # validOptions copied from kw ] for key in validOptions: #-- 2 body -# [ key is a string -> # if key is in kw -> # aux[key] := kw[key] # else -> I ] try: aux[key] = kw[key] except KeyError: pass #-- 3 -aux[TAGS_OPTION] = tagList #-- 4 -return aux 13.10. Rudiment.__str__() tkscene.py # - - - R u d i m e n t . _ _ s t r _ _ def __str__(self): '''Return a string representation of self. ''' cards = ' '.join( [ "%s=%s" % (name, str(self.p[name])) for name in self.p.keys() ] ) return ( "<Rudiment(%s)>\n p(%s)\n aux=(%r))" % (self.tagList, cards, self.aux) ) 14. class StraightRudiment: One line segment An instance of this class represents a single line segment in scene space. Here are the cardinal points: Figure 1. Geometry of a straight rudiment y width PT_W PT_A PT_B x New Mexico Tech Computer Center tkscene: A scene geometry manager 27 For a discussion of why these points are stored inside a Cardinal instance self.p inherited from the base class, and why widths are represented as a pair of points, see Section 6, “Design of the Rudiment class” (p. 6). The points whose names in the Cardinal class are self.PT_A and self.PT_B are the ends of the line segment to be drawn. If a line width option width=W is specified, we also define a cardinal point named self.PT_W which is initially located a distance W east (that is, in the +x direction) of PT_A. (PT_W is actually defined in Section 13, “class Rudiment: Scene display component” (p. 21).) In the .tkDraw() method, if point PT_W is defined, the rendered width will be the distance between PT_A and PT_W. If PT_W is undefined, the actual width will be one pixel. Because of the constraint that all class constructors of subclasses of Rudiment must have exactly the same arguments (see Section 6, “Design of the Rudiment class” (p. 6)), this class provides a static make() method that takes arguments in a more convenient form and converts them into the Cardinals and auxiliary dictionary required by the Rudiment() constructor. tkscene.py # - - - - - c l a s s S t r a i g h t R u d i m e n t class StraightRudiment(Rudiment): '''Line segment in scene space. Exports: StraightRudiment(tagList, c, **aux): [ (arguments are as in Rudiment() -> return a new StraightRudiment instance with those values ] StraightRudiment.make ( tagList, p1, p2, fill='black', width=None ): # Static method [ (tagList is as in Rudiment()) and (p1 and p2 are endpoints in scene space as Pt instances) and (fill is a line color as a Tkinter color) and (width is the line width in scene dimensions) -> return a StraightRudiment representing those values, with a minimum line width of 1 pixel ] .PT_A: [ name of the starting point ] .PT_B: [ name of the endpoint ] ''' PT_A = 'A' PT_B = 'B' DEFAULT_OPTIONS = { 'fill': 'black', 'width': None } 14.1. StraightRudiment.__init__() tkscene.py # - - - S t r a i g h t R u d i m e n t . _ _ i n i t _ _ def __init__(self, tagList, c, **aux): '''Constructor ''' Rudiment.__init__(self, tagList, c, **aux) 28 tkscene: A scene geometry manager New Mexico Tech Computer Center 14.2. StraightRudiment.make(): Factory method tkscene.py # - - - S t r a i g h t R u d i m e n t . m a k e @staticmethod def make ( tagList, p1, p2, **kw ): '''Factory method. ''' If this is the first class derived from Rudiment that you have seen, you need to know that the cardinal points that define the shape of a rudiment are stored in a Cardinals instance named c, which mostly works like a dictionary where the points are stored as homcoord.Pt instances with their names as keys. In this particular class, there are two points named StraightRudiment.PT_A and StraightRudiment.PT_B stored there. For a discussion of why we can't just store them in an attribute, see Section 6, “Design of the Rudiment class” (p. 6). The p1 and p2 arguments become those two named points, but the keyword arguments kw go in two directions. The fill= value, specifying the line color, is routed into the keyword arguments to the base class constructor. The width dimension, however, is subject to being transformed, so it is represented as a point StraightRudiment.PT_W, whose distance from point PT_A defines the width. tkscene.py #-- 1 -c = Cardinals ( ** { StraightRudiment.PT_A: p1, StraightRudiment.PT_B: p2 } ) See Section 13.9, “Rudiment.filterOptions(): Set up Tkinter option values” (p. 26). tkscene.py #-- 2 -# [ aux := a new dictionary containing the entries from # self.DEFAULT_OPTIONS whose values are not None, # updated by the entries from kw whose keys are in # self.DEFAULT_OPTIONS ] aux = Rudiment.filterOptions ( tagList, kw, StraightRudiment.DEFAULT_OPTIONS ) See Section 13.7, “Rudiment.setWidth(): Store the width value” (p. 25). tkscene.py #-- 3 -# [ if kw has a WIDTH_OPTION key -> # c := c with a point named Rudiment.PT_W defined # at a distance w from p1 in the +x direction # else -> I ] Rudiment.setWidth ( c, p1, kw ) #-- 3 -return StraightRudiment(tagList, c, **aux) New Mexico Tech Computer Center tkscene: A scene geometry manager 29 14.3. StraightRudiment.tkDraw(): Render it tkscene.py # - - - S t r a i g h t R u d i m e n t . t k D r a w def tkDraw(self, can): '''Render self onto a canvas. ''' #-- 1 -# [ can := can - (elements whose oids are in self.__oidList) # self.__oidList := a new, empty list ] self.erase(can) Straight lines are rendered using Tkinter's Canvas.create_line() method. Keyword options to .create_line() come from the base class's .aux dictionary, including tags, but there is one we must add at rendering time: the line width. If a width was passed to our constructor, it was stored as two points named PT_ORIG and PT_W; if self.p does not contain a point named PT_W, we'll use the default width: one pixel. tkscene.py #-- 2 -a,b = self.p[self.PT_A], self.p[self.PT_B] kw = dict(self.aux) #-- 3 -# [ if self has a point named self.PT_W -> # kw[WIDTH_OPTION] := the distance between (a) and # that point, but not less than 1 # else -> I ] self.getWidth(kw, a) try: if kw[WIDTH_OPTION] is None: del kw[WIDTH_OPTION] except KeyError: pass See Section 21, “pointsTuple(): Build a Tkinter points tuple” (p. 50) for the function that converts the endpoints into a tuple (x0, y0, x1, y1, …) tkscene.py #-- 4 -# [ can := can + (a new line segment with endpoints (a) # and (b) and options kw) # self.__oidList +:= object ID of that line ] points = pointsTuple ( [a, b] ) oid = self.addCan ( can.create_line ( *points, **kw ) ) 15. class BoxRudiment: Rectangle rudiment This graphic primitive describes a rectangle of arbitrary size, location, and rotation angle. Because it is initially unrotated, a rectangle can be specified by two points at opposite ends of either main diagonal. However, because a rudiment may be rotated, internally we store all four corners as cardinal points, named after the directions northeast, northwest, southwest, and southeast. 30 tkscene: A scene geometry manager New Mexico Tech Computer Center Figure 2. Geometry of a box rudiment outline color PT_NW PT_NE fill color PT_SW PT_SE PT_W width Also, the border width that comes in as a keyword argument width=W is not stored in the self.aux dictionary, for reasons discussed in Section 6, “Design of the Rudiment class” (p. 6). Instead, we store a point named self.PT_W that is a distance W from PT_SW; the distance between these two points will remain in proportion through scaling transformations. If no width is specified, point self.PT_W remains unset; the default border will be rendered one pixel wide. (PT_W comes from Section 13, “class Rudiment: Scene display component” (p. 21).) The other keyword arguments are stored in the inherited .aux dictionary: outline for the color of the border, and fill for the color of the interior, if any. The .make() static method accepts arguments in a convenient form, but the actual constructor must have the exact same argument list as the constructor for Rudiment(), for reasons discussed in Section 6, “Design of the Rudiment class” (p. 6). tkscene.py # - - - - - c l a s s B o x R u d i m e n t class BoxRudiment(Rudiment): '''Represents a rectangle with arbitrary position and rotation. Exports: BoxRudiment(tagList, c, *aux): [ arguments are as in Rudiment() -> return a new BoxRudiment instance with those values ] BoxRudiment.make(tagList, p1, p2, fill='', outline='black', width=None): # Static method [ (tagList is a list of tags to be applied to Tkinter canvas objects) and (p1 and p2 are opposite corners of a rectangle in scene space, as Pt instances) and (fill is the interior color of the rendered box, as a Tkinter color, with '' meaning transparent) and (outline is the border color of the box, defaulting to black) and (width is the border width of the box, with None interpreted as 1 pixel, and 0 meaning no border) -> return a new BoxRudiment representing those values ] .PT_NE: [ name of the original upper right corner ] .PT_NW, .PT_SW, .PT_SE: [ other original corners ] New Mexico Tech Computer Center tkscene: A scene geometry manager 31 ''' PT_NE, PT_NW, PT_SW, PT_SE = 'NE', 'NW', 'SW', 'SE' VERTICES = (PT_NE, PT_NW, PT_SW, PT_SE) DEFAULT_OPTIONS = { FILL_OPTION: '', OUTLINE_OPTION: 'black' } 15.1. BoxRudiment.__init__() tkscene.py # - - - B o x R u d i m e n t . _ _ i n i t _ _ def __init__ ( self, tagList, c, **aux ): '''Constructor. ''' Rudiment.__init__(self, tagList, c, **aux) 15.2. BoxRudiment.make() tkscene.py # - - - B o x R u d i m e n t . m a k e @staticmethod def make ( tagList, p1, p2, **kw ): '''Factory method. ''' The coordinates p1 and p2 are always in standard position, meaning that the box's sides are parallel to the x and y axes. We use the Box() constructor to standardize the point positions so that our local corners will be the lower left and upper right. tkscene.py #-- 1 -# [ baseBox := a Box whose corners are p1 and p2 ] baseBox = Box(p1, p2) Next we build the Cardinals instance where the cardinal points of the figure are stored. Refer to the figure in Section 15, “class BoxRudiment: Rectangle rudiment” (p. 30) for the geometry. tkscene.py #-- 2 -# [ c := a Cardinals object with points PT_SW=baseBox.cMin, # PT_NE=baseBox.cMax, PT_NW=upper left corner of that # box, and PT_SE=lower right corner of that box ] nw = Pt ( baseBox.cMin.x(), baseBox.cMax.y() ) se = Pt ( baseBox.cMax.x(), baseBox.cMin.y() ) c = Cardinals ( ** { BoxRudiment.PT_NE: baseBox.cMax, BoxRudiment.PT_NW: nw, BoxRudiment.PT_SW: baseBox.cMin, BoxRudiment.PT_SE: se } ) #-- 3 -# [ if kw has a WIDTH_OPTION key -> # c := c with a point named Rudiment.PT_W defined # at a distance kw[WIDTH_OPTION] from 32 tkscene: A scene geometry manager New Mexico Tech Computer Center # baseBox.cMin in the +x direction # else -> I ] Rudiment.setWidth ( c, baseBox.cMin, kw ) Our intended function doesn't specify what to do with options other than the expected two, outline and fill. We'll just silently ignore them. The expected ones get passed on for the .aux attribute. Also added to this dictionary is a tags value that comes from tagList. tkscene.py #-- 4 -# [ aux := a new dictionary containing the entries from # self.DEFAULT_OPTIONS whose values are not None, # updated by the entries from kw whose keys are in # self.DEFAULT_OPTIONS ] aux = Rudiment.filterOptions(tagList, kw, BoxRudiment.DEFAULT_OPTIONS) After all that, we're ready to call the base class constructor. tkscene.py #-- 5 -return BoxRudiment(tagList, c, **aux) 15.3. BoxRudiment.tkDraw() tkscene.py # - - - B o x R u d i m e n t . t k D r a w def tkDraw(self, can): '''Render self onto a canvas. ''' We can't use Tkinter's Canvas.create_rectangle() primitive, because that can only draw boxes whose sides are parallel to the coordinate axes. So we'll just call it a polygon. The Canvas.create_polygon() method defines a polygon by its vertices—four of them, in this case. tkscene.py #-- 1 -# [ can := can - (all the objects whose object IDs are # in self.__oidList) ] self.erase(can) #-- 2 -# [ cornerTuple := list of the four corner points as # (x0, y0, x1, y1, ...) # p0 := point self.PT_SW # kw := copy of self.aux ] cornerTuple = pointsTuple ( [ self.p[key] for key in self.VERTICES ] ) p0 = self.p[self.PT_SW] kw = dict(self.aux) Next we have to set up the keyword attributes. New Mexico Tech Computer Center tkscene: A scene geometry manager 33 • The fill and outline attributes hav the same meanings in Tkinter: interior color (default '', meaning transparent) and border color. • The width is the border width. If the cardinal point self.PT_W is defined, the distance from that point to self.PT_SW specifies the border width. The default value is one pixel. See RudimentgetWidth. tkscene.py #-- 3 -# [ if self has a point named Rudiment.PT_W -> # kw[WIDTH_OPTION] := the distance from p0 to that # point, but not less than 1 ] self.getWidth ( kw, p0 ) #-# [ # # oid 4 -can := can with a new canvas polygon element added, with corners given by (cornerTuple) and options (kw) self.__oidList +:= that element ] = self.addCan ( can.create_polygon ( *cornerTuple, **kw ) ) 16. class OvalRudiment: Ellipse What Tkinter calls an oval is actually an ellipse. Geometrically, an oval is defined by two points on opposite corners of a bounding box whose sides are parallel to the coordinate axes. Although the bounding boxes of Tkinter's canvas oval objects are always oriented in this way, with their axes parallel to the coordinate system, we can support arbitrary rotation of ellipses by abusing the canvas polygon widget. If you render a polygon that happens to be a rectangle, in any orientation, and use the smooth=1 option, the result is an ellipse that fits that bounding box. The names of the four points of the bounding box are the same as in Section 15, “class BoxRudiment: Rectangle rudiment” (p. 30): PT_SW for the southwest corner, PT_NW for the northeast corner, and so forth. tkscene.py # - - - - - c l a s s O v a l R u d i m e n t class OvalRudiment(Rudiment): '''Ellipse rudiment. Exports: OvalRudiment(tagList, cardinals, **kw): [ arguments are as in Rudiment() -> return a new OvalRudiment instance with those values ] OvalRudiment.make(tagList, p1, p2, fill='', outline='black', width=None): [ (tagList is a list of tags to be applied to Tkinter canvas objects) and (p1 and p2 are opposite corners of the bounding box of an ellipse whose axes are parallel to the x- and y-axes) and (fill is the interior color of the rendered ellipse, with '' meaning transparent) and (outline is the border color of the ellipse, defaulting to black) and 34 tkscene: A scene geometry manager New Mexico Tech Computer Center (width is the border width of the box, with None interpreted as 1 pixel, and 0 meaning no border) -> return a new OvalRudiment with those values ] .PT_NE, .PT_SE, .PT_SW, .PT_NW: [ names of the corner points of the bounding box ] .VERTICES: [ sequence of the corner point names in order ] .FUZZ: [ distances less than this amount are considered to be close enough in .tkDraw() ] ''' PT_NE, PT_SE, PT_SW, PT_NW = 'NE', 'SE', 'SW', 'NW' VERTICES = (PT_NE, PT_SE, PT_SW, PT_NW) DEFAULT_OPTIONS = { FILL_OPTION: '', OUTLINE_OPTION: 'black' } FUZZ = 1.5 16.1. OvalRudiment.__init__() A straight pass-through; all derived class constructors must be identical to that of the base class, as explained in Section 6, “Design of the Rudiment class” (p. 6). tkscene.py # - - - O v a l R u d i m e n t . _ _ i n i t _ _ def __init__(self, tagList, c, **aux): '''Constructor ''' Rudiment.__init__(self, tagList, c, **aux) 16.2. OvalRudiment.make() Ellipses start out parallel to the coordinate axes, so we need only two points to define them. However, in order to support arbitrary rotation, we store all four vertices of the bounding box. See Section 20, “class Box: Basic rectangle” (p. 48). tkscene.py # - - - O v a l R u d i m e n t . m a k e @staticmethod def make(tagList, p1, p2, **kw): '''Oval factory. ''' #-- 1 -# [ box := a new Box instance whose main diagonal is # defined by points p1 and p2 # nw := northwest corner of that box # se := southeast corner of that box box = Box ( p1, p2 ) nw = Pt ( box.cMin.x(), box.cMax.y() ) se = Pt ( box.cMax.x(), box.cMin.y() ) See Section 19, “class Cardinals: A transformable collection of named points” (p. 47). New Mexico Tech Computer Center tkscene: A scene geometry manager 35 tkscene.py #-- 2 -c = Cardinals ( ** { OvalRudiment.PT_NE: OvalRudiment.PT_SE: OvalRudiment.PT_SW: OvalRudiment.PT_NW: box.cMax, se, box.cMin, nw } ) Next we check for a width option. See Section 13.7, “Rudiment.setWidth(): Store the width value” (p. 25). tkscene.py #-- 3 -# [ if kw has a WIDTH_OPTION key -> # c := c with a point named Rudiment.PT_W defined # at a distance kw[WIDTH_OPTION] from # box.cMin in the +x direction # else -> I ] Rudiment.setWidth ( c, box.cMin, kw ) Next we copy the user's options to a new dictionary aux, omitting any we don't expect (anything except fill and outline), and add a tags option. tkscene.py #-- 4 -# [ aux := a new dictionary containing the entries from # self.DEFAULT_OPTIONS whose values are not None, # updated by the entries from kw whose keys are in # self.DEFAULT_OPTIONS ] aux = Rudiment.filterOptions(tagList, kw, OvalRudiment.DEFAULT_OPTIONS ) #-- 5 -return OvalRudiment(tagList, c, **aux) 16.3. OvalRudiment.tkDraw() There are three different rendering cases. 1. If the bounding box is parallel to the coordinate axes, we can use Tkinter's canvas oval object. In practice, if either the Δx or the Δy is less than 1.5 (pixels), the bounding box is parallel or close enough. 2. If the bounding box is not parallel to the coordinate axes, and it is not square, then we can't use Tkinter's canvas oval object, because it does not support arbitrary rotation of ellipses. Instead, we use the canvas polygon object with a smooth=1 option, which fits a closed spline into the box. This is not perfect but at least looks close for most cases. In practice, if the difference between the lengths of the first two sides of the bounding box is greater than 1.5, we use the smoothed polygon substitution. 3. If the bounding box is a square, the desired rendering is a circle. However, if we use the smoothed polygon trick, the result looks distinctly uncircular. In this case, since the rotation doesn't matter, we calculate the circle's center as the midpoint of two vertices opposite each other on the main diagonal, calculate the radius as half that, and construct a new, square bounding box parallel to the coordinate axes and use the Tkinter canvas oval object to render the circle. 36 tkscene: A scene geometry manager New Mexico Tech Computer Center tkscene.py # - - - O v a l R u d i m e n t . t k D r a w def tkDraw ( self, can ): '''Render an oval on a Tkinter canvas. ''' #-- 1 -# [ vList := the four vertices of the bounding box in # clockwise order # a, b, c, d := those same four vertices # sw := self.p[self.PT_SW] # deltaP := difference between the first two vertices # deltaL := (distance from first vertex to second) # (distance from second vertex to third) ] a, b, c, d = vList = [ self.p[vName] for vName in self.VERTICES ] sw = self.p[self.PT_SW] deltaP = a - b deltaL = abs ( a.dist(b) - b.dist(c) ) When we use Tkinter's oval object, see Section 16.4, “OvalRudiment.__drawOval()” (p. 39). For the polygon case, see Section 16.5, “OvalRudiment.__drawPolygon()” (p. 39). The class constant self.FUZZ is used to test whether two points are close enough. tkscene.py #-- 2 -# [ if (abs(deltaP.x()) < self.FUZZ) or # (abs(deltaP.y()) < self.FUZZ) -> # can := can - (display elements pertaining to self) + # (a new canvas oval with main diagonal from a to c, # and attributes from self.aux and (sw) # else if (deltaL < self.FUZZ) -> # can := can - (display elements pertaining to self) + # (a new canvas oval whose center is the midpoint # between points a and c and whose radius is half # the distance from a to c, and attributes from # self.aux and (sw)) # else -> # can := can - (display elements pertaining to self) + # (a new, smoothed canvas polygon with vertices # (vList), and attributes from self.aux and (sw)) ] if ( ( abs(deltaP.x()) < self.FUZZ ) or ( abs(deltaP.y()) < self.FUZZ ) ): points = pointsTuple ( [a, c] ) self.__drawOval ( can, points, sw ) elif deltaL < self.FUZZ: center = Pt ( (a.x() + c.x())/2.0, (a.y() + c.y())/2.0 ) radius = a.dist(center) points = pointsTuple ( [ Pt ( center.x()-radius, center.y()-radius ), Pt ( center.x()+radius, center.y()+radius ) ] ) self.__drawOval ( can, points, sw ) else: New Mexico Tech Computer Center tkscene: A scene geometry manager 37 points = pointsTuple ( vList ) self.__drawPolygon ( can, points, sw ) tkscene.py #-- 1 -# [ sw := the original southwest corner of the bounding box # as a homcoord.Pt # points := the vertices of the bounding box as a Tkinter # points tuple # kw := a copy of self.aux ] sw = self.p[self.PT_SW] points = pointsTuple ( [ self.p[vName] for vName in self.VERTICES ] ) kw = dict(self.aux) See Section 13.8, “Rudiment.getWidth(): Set up Tkinter's border width” (p. 25) for the logic that sets up the border width. tkscene.py #-- 2 -# [ if self has a point named Rudiment.PT_W -> # kw[WIDTH_OPTION] := the distance from (sw) to that # point, but not less than 1 ] self.getWidth ( kw, sw ) #-- 3 -For the management of canvas object IDs, see Section 13.4, “Rudiment.addCan(): Add the oid of a canvas object” (p. 24). tkscene.py #-# [ # # # # # # # p1, 4 -if (the sides are not parallel to the axes) and (the lengths of the sides differ by more than one pixel) -> self := self with a new canvas polygon added with points (points) and options (kw) else -> self := self with a new circular oval added whose center is the center of self.VERTICES and whose diameter is the length of a side ] p2, p3 = [ self.p[self.VERTICES[i]] for i in range(3) ] side1 = p1.dist(p2) side2 = p2.dist(p3) diff = abs(side1 - side2) if diff > 1.5: kw[SMOOTH_OPTION] = '1' oid = self.addCan ( can.create_polygon ( *points, **kw ) ) else: radius = side1 / 2.0 center = Pt ( (p1.x()+p3.x())/2.0, (p1.y()+p3.y())/2.0 ) points = pointsTuple ( [ Pt ( center.x()-radius, center.y()-radius ), 38 tkscene: A scene geometry manager New Mexico Tech Computer Center Pt ( center.x()+radius, center.y()+radius ) ] ) oid = self.addCan ( can.create_oval ( *points, **kw ) ) 16.4. OvalRudiment.__drawOval() This method renders the cases that fit Tkinter's canvas oval object. The widthRef argument is the point whose distance from point PT_W defines the border width. tkscene.py # - - - O v a l R u d i m e n t . _ _ d r a w O v a l def __drawOval ( self, can, points, widthRef ): '''Render this rudiment using Tkinter's canvas oval. [ (can is a Tkinter.Canvas) and (points is the diagonal of the bounding box as a tuple (x0, y0, x1, y1)) and (widthRef is the point relative to which PT_W defines the border width) -> can := can - (display elements pertaining to self) + (a new canvas oval with bounding box (points) and attributes from self.aux and widthRef) ] ''' #-- 1 -# [ kw := a copy of self.aux ] kw = dict(self.aux) See Section 13.8, “Rudiment.getWidth(): Set up Tkinter's border width” (p. 25) for the logic that sets up the border width. tkscene.py #-- 2 -# [ if self has a point named Rudiment.PT_W -> # kw[WIDTH_OPTION] := the distance from (widthRef) # to that point, but not less than 1 ] self.getWidth ( kw, widthRef ) #-- 3 -# [ self := self with a new canvas oval added with # points (points) and attributes (kw) ] oid = self.addCan ( can.create_oval(*points, **kw) ) 16.5. OvalRudiment.__drawPolygon() This method implements the substitute rendering that must be used when the oval is not circular and when its bounding box is not parallel to the coordinate axes. See Section 16.3, “OvalRudiment.tkDraw()” (p. 36) for further discussion, and compare Section 16.4, “OvalRudiment.__drawOval()” (p. 39) for the setup of the kw dictionary. tkscene.py # - - - O v a l R u d i m e n t . _ _ d r a w P o l y g o n def __drawPolygon ( self, can, points, ref ): New Mexico Tech Computer Center tkscene: A scene geometry manager 39 '''Render a rotated ellipse as a smoothed polygon. [ (can is a Tkinter.Canvas) and (points are the four vertices of a bounding box as a Tkinter points tuple) and (widthRef is the point relative to which PT_W defines the border width) -> can := can - (display elements pertaining to self) + (a new, smoothed canvas polygon with bounding box (points) and attributes from self.aux and widthRef) ] ''' #-- 1 -# [ kw := a copy of self.aux ] kw = dict(self.aux) #-- 2 -# [ if self has a point named Rudiment.PT_W -> # kw[WIDTH_OPTION] := the distance from (ref) to that # point, but not less than 1 ] self.getWidth ( kw, ref ) A minor Python point about the call to create_polygon: if one provides both explicit keyword arguments, and a prefabricated set of keywords using the “**” convention, the method receives a dictionary containing both, so long as no key occurs in both places. tkscene.py #-- 3 -# [ self := self with a new smoothed canvas added with # points (points) and attributes (kw) ] oid = self.addCan ( can.create_polygon ( *points, smooth=1, **kw ) ) 17. class ArcRudiment: Circular arc This rudiment is used for drawing pieces of circles. An arc is initially specified by three points: the center of the base circle, the point where the arc begins, and the point where the arc ends. Warning It is possible to apply a transform to a circular arc that produces a conic section that cannot be rendered as a Tkinter Canvas arc. This class does not guarantee correct rendering for segments of rotated ellipses, which could result from a scaling transform with different values for the x and y scale factors, followed by a rotation transform. You may transform this rudiment all you want, but when it comes time to render, the center will be at PT_C, the radius and initial angle will be determined by PT_A, and the final angle will be determined by PT_B. Here is the initial geometry of the cardinal points. 40 tkscene: A scene geometry manager New Mexico Tech Computer Center Figure 3. Geometry of an arc rudiment PT_B PT_A PT_C PT_W width tkscene.py # - - - - - c l a s s A r c R u d i m e n t class ArcRudiment(Rudiment): '''Represents a circular arc. Exports: ArcRudiment(tagList, c, **kw): [ arguments are as in Rudiment() -> return a new ArcRudiment with those values ] ArcRudiment.make(tagList, center, fromPoint, toPoint, outline='black', width=None): # Static method [ (tagList is a sequence of tags to apply to elements) and (center is the arc's center in scene space as a Pt) and (fromPoint is the arc's start point as a Pt) and (toPoint is the arc's end point (counterclockwise) as a Pt) and (outline is the arc's color as a Tkinter color) and (width is the arc's width, defaulting to 1 pixel) -> return a new ArcRudiment representing those values ] PT_A: [ name of the arc's start point in self.p ] PT_B: [ name of the arc's start point in self.p ] PT_C: [ name of the arc's center point in self.p ] State/Invariants: .__corners: [ a tuple (x0, y0, x1, y1) where (x0, y0) is the upper left corner of the circle's bounding box and (x1, y1) is the lower right corner ] .__options: [ dictionary of Canvas.create_arc() options for self ] ''' PT_A = 'A'; PT_B = 'B'; PT_C = 'C' DEFAULT_OPTIONS = { OUTLINE_OPTION: 'black' } The mathematically astute reader may note the assumption that points PT_A and PT_B are both at the same distance from the center point PT_C. In practice, we will take the distance from PT_C to PT_A as New Mexico Tech Computer Center tkscene: A scene geometry manager 41 the radius. The angles will be defined as the angles of rays from the center to the two endpoints, so point PT_B will serve only to define the ending angle of the arc. Thus, if in practice the radii to the two points differ slightly, the graphics rendering should still be fairly close. 17.1. ArcRudiment.__init__() tkscene.py # - - - A r c R u d i m e n t . _ _ i n i t _ _ def __init__ ( self, tagList, c, **aux ): '''Constructor. ''' Rudiment.__init__(self, tagList, c, **aux) 17.2. ArcRudiment.make(): Circular arc tkscene.py # - - - A r c R u d i m e n t . _ _ i n i t _ _ @staticmethod def make ( tagList, center, fromPoint, toPoint, **kw): '''Factory method. ''' First instantiate the Cardinals instance to hold the points, and populate it with the values we know now. Then check for a width keyword argument and, if it is found, use that width to define the position of point PT_W. tkscene.py #-- 1 -c = Cardinals ( { ArcRudiment.PT_C: center, ArcRudiment.PT_A: fromPoint, ArcRudiment.PT_B: toPoint } ) #-- 2 -# [ if kw has a WIDTH_OPTION key -> # c := c with a point named Rudiment.PT_W # defined at a distance kw[WIDTH_OPTION] from # center in the +x direction # else -> I ] Rudiment.setWidth(c, center, kw) The aux dictionary holds the outline keyword argument that specifies the color of the arc. tkscene.py #-- 3 -# [ aux := a new dictionary containing the entries from # self.DEFAULT_OPTIONS whose values are not None, # updated by the entries from kw whose keys are in # self.DEFAULT_OPTIONS ] aux = Rudiment.filterOptions(tagList, kw, ArcRudiment.DEFAULT_OPTIONS ) 42 tkscene: A scene geometry manager New Mexico Tech Computer Center #-- 4 -return ArcRudiment(tagList, c, **aux) 17.3. ArcRudiment.tkDraw() tkscene.py # - - - A r c R u d i m e n t . t k D r a w def tkDraw(self, can): '''Render self on a canvas. ''' #-- 1 -# [ can := can - (elements pertaining to self) ] self.erase(can) To draw an arc in the Tkinter world, first define the bounding box of the circle by two endpoints of its main diagonal. We'll use as endpoints (center-radius, center-radius) and (center+radius, center+radius). tkscene.py #-# [ # # # # # c = a = b = cx, r = 2 -c := point PT_C a := point PT_A b := point PT_B cx := x coordinate of PT_C cy := y coordinate of PT_C r := distance between points PT_C and PT_A ] self.p[self.PT_C] self.p[self.PT_A] self.p[self.PT_B] cy = c.xy() c.dist ( self.p[self.PT_A] ) #-- 3 -# [ points := a tuple (c_x-r, c_y-r, c_x+r, c_y+r) ] points = pointsTuple ( [ Pt(cx-r, cy-r), Pt(cx+r, cy+r) ] ) Tkinter wants as its start argument the initial bearing, and as its extent argument the difference in bearings, both in degrees. One subtlety: since we are by definition in display coordinates, and +y is the opposite direction, the angles and extent values must be negated. tkscene.py #-- 4 -# [ start := Cartesian angle from (cx, cy) to PT_A in degrees # extent := (Cartesian angle from (cx, cy) to PT_B in # degrees) - angle from (cx, cy) to PT_A ] start = - degrees ( c.bearing(a) ) finish = - degrees ( c.bearing(b) ) extent = finish - start Next we assemble as a dictionary kw all the keyword options to the Canvas.create_arc() method. If there is a width reference point PT_W, we add a width option to that dictionary. New Mexico Tech Computer Center tkscene: A scene geometry manager 43 tkscene.py #-- 5 -# [ kw := a new dictionary with options copied from self.aux # plus start=(start), extent=(extent), and style=ARC ] kw = dict(self.aux) kw.update ( { 'start': start, 'extent': extent, 'style': ARC } ) #-- 6 -# [ if self has a point named Rudiment.PT_W -> # kw[WIDTH_OPTION] := the distance from (c) to that # point, but not less than 1 ] self.getWidth ( kw, c ) #-- 7 -# [ can := can + (a new arc with bounding box described by # (points) with options (kw) ] oid = self.addCan ( can.create_arc ( *points, **kw ) ) 18. class PolygonRudiment: Arbitrary polygons Tkinter canvas polygons are defined by a sequence of three or more vertices. The naming convention for points is 'P_' followed by the vertex number starting with zero. The number of digits in the vertex number is determined by the number of vertices. For example, if there are between 100 and 999 vertices, they will be numbered 'P_000', 'P_001', …. There is one slightly inelegant feature of this class. The number of vertices is stored in the .aux dictionary under the key whose name is NV. The other entries in the .aux dictionary are passed to the canvas object creation method, but self.aux[self.NV] does not get passed on, since it is internal to this class. tkscene.py # - - - - - c l a s s P o l y g o n R u d i m e n t class PolygonRudiment(Rudiment): '''Arbitrary polygon rudiment. Exports: PolygonRudiment(tagList, c, **kw): [ arguments are as in Rudiment() -> return a new PolygonRudiment with those values ] PolygonRudiment.make(tagList, vertices, fill='', outline='black', width=None): [ (tagList is a sequence of tags to apply to elements) and (vertices is a sequence of vertices as homcoord.Pt instances) and (fill is the interior color as a Tkinter color, with "" meaning transparent) and (outline is the border color as a Tkinter color) and (width is the border width, defaulting to 1 pixel) -> return a new PolygonRudiment representing those values ] .PT_PREFIX: [ the prefix for all vertex point names ] 44 tkscene: A scene geometry manager New Mexico Tech Computer Center .NV: [ key in self.aux under which the vertex count is stored ] ''' PT_PREFIX = 'P_' NV = 'NV' DEFAULT_OPTIONS = { FILL_OPTION: '', OUTLINE_OPTION: 'black' } 18.1. PolygonRudiment.__init__() A pro-forma pass-through constructor; see Section 6, “Design of the Rudiment class” (p. 6). tkscene.py # - - - P o l y g o n R u d i m e n t . _ _ i n i t _ _ def __init__(self, tagList, c, **aux ): '''Constructor. ''' Rudiment.__init__(self, tagList, c, **aux) 18.2. PolygonRudiment.make() The first step is to see how many digits we need to use for the names of the vertices. tkscene.py # - - - P o l y g o n R u d i m e n t . m a k e @staticmethod def make ( tagList, vertices, **kw ): '''PolygonRudiment factory. ''' #-- 1 -# [ nVertices := number of elements in vertices # nDigits := number of digits in the number of elements in # vertices # c := a new, empty Cardinals instance ] nVertices = len(vertices) nDigits = len(str(nVertices)) Next we instant convert the vertices into named points in the Cardinals instance. For the logic that translates a vertex number to a vertex name, see Section 18.4, “PolygonRudiment.vertexNo(): Point name for a numbered vertex” (p. 47). tkscene.py #-- 2 -# [ c +:= entries whose keys are the names of the points in # (vertices) and each related value is that point ] for i in len(vertices): #-- 2 body -# [ c +:= an entry whose key is the name of the (i)th # vertex, and the related value is that vertex ] c[PolygonRudiment.vertexNo(i)] = vertices[i] The reference point for the border width is the first vertex. See Section 13.7, “Rudiment.setWidth(): Store the width value” (p. 25). New Mexico Tech Computer Center tkscene: A scene geometry manager 45 tkscene.py #-- 2 -# [ if kw has a WIDTH_OPTION key -> # c := c with a point named Rudiment.PT_W # defined at a distance kw[WIDTH_OPTION] from # center in the +x direction # else -> I ] Rudiment.setWidth(c, vertices[0], kw) Next, copy the interior and outline color options to a new dictionary if they are provided, and call the actual constructor. tkscene.py #-- 3 -# [ aux := a new dictionary containing the entries from # self.DEFAULT_OPTIONS whose values are not None, # updated by the entries from kw whose keys are in # self.DEFAULT_OPTIONS ] aux = Rudiment.filterOptions(tagList, kw, self.DEFAULT_OPTIONS ) Here is the inelegant part: passing the vertex count through the aux attribute, which is really intended for Tkinter options. But there's no easy way to discover the number of vertices by examining the names of points in the Cardinals instance. Alternative: we could use a ridiculous length for the sequence numbers (P_123456789) and just pull out points in order until we get a failure. tkscene.py #-- 4 -aux[PolygonRudiment.NV] = nVertices #-- 5 -return PolygonRudiment(tagList, c, **aux) 18.3. PolygonRudiment.tkDraw() As discussed in Section 18.2, “PolygonRudiment.make()” (p. 45), the number of vertices is passed as an entry in the self.aux dictionary using key self.NV. We remove that entry from the option dictionary passed to Canvas.create_polygon(). tkscene.py # - - - P o l y g o n R u d i m e n t . t k D r a w def tkDraw ( self, can ): '''Render self onto a canvas. ''' #-- 1 -# [ nVertices := self.aux[self.NV] # nDigits := number of digits in self.aux[self.NV] # kw := a copy of self.aux without the entry for self.NV # v0 := the first vertex from self ] nVertices = self.aux[self.NV] nDigits = len(str(nVertices)) kw = dict(self.aux) del kw[self.NV] v0 = self.p[self.vertexNo(nDigits, 0)] 46 tkscene: A scene geometry manager New Mexico Tech Computer Center For the function that converts a sequence of homcoord.Pt instances to a Tkinter points tuple, see Section 21, “pointsTuple(): Build a Tkinter points tuple” (p. 50). tkscene.py #-- 2 -# [ points := a Tkinter points tuple containing the vertices # in self number 0, 1, 2, .., (nVertices-1) ] points = pointsTuple ( [ self.p[self.vertexNo(nDigits, i)] for i in range(0, nVertices) ] ) If a width was specified, point Rudiment.PT_W specifies the width relative to the first vertex. See Section 13.8, “Rudiment.getWidth(): Set up Tkinter's border width” (p. 25). tkscene.py #-- 3 -# [ if self has a point named Rudiment.PT_W -> # kw[WIDTH_OPTION] := the distance from (v0) to that # point, but not less than 1 ] self.getWidth ( kw, v0 ) #-- 4 -# [ can := can + (a new polygon with vertices described by # (points) with options (kw) ] oid = self.addCan ( can.create_polygon ( *points, **kw ) ) 18.4. PolygonRudiment.vertexNo(): Point name for a numbered vertex The vertices of a polygon are given names consisting of the prefix PolygonRudiment.PT_PREFIX followed by the vertex's number, starting from zero, and using just enough digits for the total vertex count. This method translates a vertex number to a vertex name for storage or retrieval in the Cardinals instance. tkscene.py # - - - P o l y g o n R u d i m e n t . v e r t e x N o @staticmethod def vertexNo(nDigits, k): '''Return the name of the (k)th vertex. ''' return ( "%s%0*d" % (PolygonRudiment.PT_PREFIX, nDigits, k) ) 19. class Cardinals: A transformable collection of named points An instance of this class is basically a container for a set of named points. Each point is an instance of homcoord.Pt. For a discussion of why this class is needed, see Section 6, “Design of the Rudiment class” (p. 6). The class is basically a dictionary; in fact, it inherits from dict. It adds a method that returns a new instance with all the same names but after each point has been run through a transform. New Mexico Tech Computer Center tkscene: A scene geometry manager 47 tkscene.py # - - - - - c l a s s C a r d i n a l s class Cardinals(dict): '''For storing a set of named cardinal points. Exports: Cardinals(**kw): [ kw is a dictionary whose keys are strings and each related value is a homcoord.Pt instance -> return a new Cardinals instance containing the entries from kw ] .transform(x): [ x is a homcoord.transform instance -> return a new Cardinals instance with the same entries except each value has been transformed by x ] ''' def transform(self, x): '''Return a transformed instance. ''' #-- 1 -result = Cardinals() #-- 2 -# [ result +:= entries from self, with each value # transformed by x ] for key in self: result[key] = x.apply(self[key]) #-- 3 -return result A note on side effects: if you pass a pre-constructed set of keyword arguments to a function using the f(**kw) convention, a copy of that dictionary is made inside the called function. So, if you pass a dictionary to this constructor, you don't have to worry about the constructor changing values in that dictionary. 20. class Box: Basic rectangle An instance of this class represents a rectangular area. Applications include bounding boxes as well as displayed rectangles. tkscene.py # - - - - - c l a s s B o x class Box(object): '''Represents a rectangle. Exports: Box(c1, c2): [ c1 and c2 are homcoord.Pt instances representing diagonally 48 tkscene: A scene geometry manager New Mexico Tech Computer Center opposite corners of a rectangle -> return a new Box representing that .cMin: [ minimum x and y coordinates as .cMax: [ maximim x and y coordinates as .__contains__( p ): [ p is a homcoord.Pt instance -> if p is within self, including the return True else -> return False ] .sizes(): [ returns (width, height) ] .aspect(): [ returns the ratio of width:height as rectangle ] homcoord.Pt instances ] homcoord.Pt instances ] boundaries -> a float ] Because the corner points may be either pair of corners, we compute the minima and maxima separately for convenient testing. tkscene.py State/Invariants: self.__xMin: [ self.__xMax: [ self.__yMin: [ self.__yMax: [ ''' minimum maximum minimum maximum x x y y coordinate coordinate coordinate coordinate ] ] ] ] 20.1. Box.__init__(): Constructor tkscene.py # - - - B o x . _ _ i n i t _ _ def __init__(self, c1, c2): '''Constructor for a box with diagonal c1-c2. ''' x1, y1 = c1.xy() x2, y2 = c2.xy() self.__xMin = min(x1, x2) self.__xMax = max(x1, x2) self.__yMin = min(y1, y2) self.__yMax = max(y1, y2) self.cMin = Pt(self.__xMin, self.__yMin) self.cMax = Pt(self.__xMax, self.__yMax) 20.2. Box.__contains__() tkscene.py # - - - B o x . _ _ c o n t a i n s _ _ def __contains__(self, p): '''Is p within the box? Lines are good. ''' x, y = p.xy() return ( ( self.__xMin <= x <= self.__xMax ) and ( self.__yMin <= y <= self.__yMax ) ) New Mexico Tech Computer Center tkscene: A scene geometry manager 49 20.3. Box.sizes(): Return the sizes of the sides tkscene.py # - - - B o x . s i z e s def sizes(self): '''Return (width, height) ''' return ( self.__xMax - self.__xMin, self.__yMax - self.__yMin ) 20.4. Box.aspect(): Compute the aspect ratio tkscene.py # - - - B o x . a s p e c t def aspect(self): '''Return the box's width:height ratio. ''' w, h = self.sizes() return float(w) / float(h) 21. pointsTuple(): Build a Tkinter points tuple Many Tkinter Canvas methods require argument lists that specify two or more points as a sequence of positional arguments x0, y0, x1, y1, …. This service function converts a sequence of homcoord.Pt instances into a tuple that can be passed to the function as a pre-built set of positional arguments using the f(*p) calling convention. tkscene.py # - - - p o i n t s T u p l e def pointsTuple(pointsList): '''Build a Tkinter points argument set. [ pointsList is a sequence of homcoord.Pt instances -> return a tuple(pointsList[0].x(), pointsList[0].y(), pointsList[1].x(), pointsList[1].y(), ... ] ''' result = [] for pt in pointsList: result.append(round(pt.x())) result.append(round(pt.y())) return tuple(result) 22. An example: conference Here is a fully worked-out example script that renders a floor plan with various items of furniture on it. In this conference room, a three-seat couch and two easy chairs are placed around a round table on a stage, and the rest of the room is filled with rows of cheap, uncomfortable folding chairs. 50 tkscene: A scene geometry manager New Mexico Tech Computer Center 0 10 20 30 20 COUCH_ARM_WIDE MAX_CHAIR_ANGLE 10 COUCH_MARGIN COUCH_SEAT_WIDE COUCH_SEAT_LONG TABLE_RADIUS AISLE 0 AISLE ROOM_HIGH CHEAP_MARGIN CHAIR_SPACING AISLE CHEAP_WIDE CHEAP_LONG ROOM_WIDE To illustrate how the scene may be animated, the Tkinter application will have a Scale widget that can rotate the two easy chairs around the center of the round table. 22.1. Prologue conference #!/usr/bin/env python #================================================================ # conference: Test driver for tkscene: conference hall setup. # Do not edit this file. It is extracted automatically from the New Mexico Tech Computer Center tkscene: A scene geometry manager 51 # documentation: # http://www.nmt.edu/tcc/help/lang/python/examples/tkscene/ #---------------------------------------------------------------- 22.2. Module imports This application is built on a number of layers: sys is the universal system interface; numpy is conference # - - - - - I m p o r t s import sys import homcoord as h from Tkinter import * from tkscene import * 22.3. Manifest constants conference # - - - - - M a n i f e s t c o n s t a n t s #-# Fonts #-BUTTON_FONT = ("Helvetica", 20) MONO_FONT = ("DejaVu Sans Mono", 12) #-# Tags to be attached to canvas items #-COUCH_TAG = "c" TABLE_TAG = "t" #-# Assorted dimensions. Scene units are feet. #-CAN_WIDE = 1200 # Width of the canvas CAN_HIGH = 800 # Height of the canvas DISPLAY_BOUNDS = Box(h.Pt(0, 0), h.Pt(CAN_WIDE, CAN_HIGH)) MAX_CHAIR_ANGLE = 45.0 # Max chair rotation from front, degrees #-# Dimensions of the stage area. Origin is the stage right, # downstage corner; downstage is toward the bottom of the display. #-def feetInches(feet, inches=0.0): '''Convert feet and inches to decimal feet. [ (feet is a number in feet) and 52 tkscene: A scene geometry manager New Mexico Tech Computer Center (inches is a number in inches) -> return the equivalent length in decimal feet ] ''' return feet+float(inches)/12.0 STAGE_WIDE = feetInches(20) # x dimension and... STAGE_LONG = feetInches(15) # y dimension of the stage MARGIN = feetInches(2) # Extra space around the scene SCENE_BOUNDS = Box(h.Pt(-MARGIN, -MARGIN), h.Pt(STAGE_WIDE+MARGIN, STAGE_LONG+MARGIN)) #-# General house and stage dimensions. #-AISLE = feetInches(3) # Handicap-accessible aisle width TABLE_RADIUS = feetInches(3, 6) # Radius of the round table onstage TABLE_COLOR = "#884411" # Table color: dark brown COUCH_MARGIN = feetInches(2) # Space between table and couch 22.4. Main program See Section 22.5, “class App: The application as a whole” (p. 53). conference # - - - - - m a i n def main(): '''Main program: display a conference room floor plan. ''' app = App() app.master.title("conference: A tkscene example.") app.mainloop() 22.5. class App: The application as a whole conference # - - - - - c l a s s A p p class App(Frame): '''Application as a whole. State/Invariants: .can: [ the main Canvas ] .angleVar: [ a DoubleVar for .angleScale ] .angleScale: [ a scale with range [0,MAX_CHAIR_ANGLE] ] .quitButton: [ a quit Button ] .tableCenter: [ center point of the round table ] .__scene: [ a tkscene.Scene instance describing the mapping from scene to display spaces ] New Mexico Tech Computer Center tkscene: A scene geometry manager 53 .couch1: .couch3: .roundTable: .leftChair: .rightChair: [ [ [ [ [ a one-seat Couch instance ] a three-seat Couch instance ] a RoundTable instance ] the left-hand moving chair as a PlacedElt ] the right-hand moving chair as a PlacedElt ] Grid plan: +-------------+ 0 | .can | +-------------+ 1 | .angleScale | +-------------+ 2 | .quitButton | +-------------+ ''' # - - - A p p . _ _ i n i t _ _ def __init__(self, master=None): Frame.__init__(self, master) self.grid() self.__createWidgets() self.__buildScene() print "@@@", self.__scene self.__scene.tkDraw(self.can) 22.6. App.__createWidgets(): Set up Tkinter widgets conference # - - - A p p . _ _ c r e a t e w i d g e t s def __createWidgets(self): '''Place all widgets; create all variables. ''' self.can = Canvas ( self, width=CAN_WIDE, height=CAN_HIGH ) rowx, colx = 0, 0 self.can.grid(row=rowx, column=colx) self.angleVar = DoubleVar() self.angleVar.set(0.0) self.angleScale = Scale ( self, command=self.__angleHandler, length="6i", from_=0.0, to=MAX_CHAIR_ANGLE, resolution=0.1, orient=HORIZONTAL, variable=self.angleVar ) rowx, colx = rowx+1, 0 self.angleScale.grid(row=rowx, column=colx, sticky=E) self.quitButton = Button ( self, text="Quit", font=BUTTON_FONT, command=self.quit ) 54 tkscene: A scene geometry manager New Mexico Tech Computer Center rowx, colx = rowx+1, 0 self.quitButton.grid(row=rowx, column=colx, sticky=E+W) 22.7. App.__buildScene(): Set up the canvas First we construct the Scene instance that relates the scene and display spaces. conference # - - - A p p . _ _ b u i l d S c e n e def __buildScene(self): '''Construct the scene on the canvas. ''' #-- 1 -# [ self.__scene := as invariant ] self.__scene = Scene(SCENE_BOUNDS, DISPLAY_BOUNDS) #-# Show the stage as a pale green rectangle #-points = pointsTuple ( [ self.__scene.s_d(Pt(0,0)), self.__scene.s_d(Pt(STAGE_WIDE, STAGE_LONG)) ] ) self.can.create_rectangle(points, fill='#efe') The logic that places elements in the scene is in Section 22.8, “App.__furnish(): Arrange the furniture” (p. 55). Once the scene is built, it is rendered onto the canvas. conference #-- 2 -# [ self.__scene +:= # position ] self.__furnish() elements of the scene in their initial 22.8. App.__furnish(): Arrange the furniture All the furniture on stage is positioned relative to the round table, which is centered in the width of the stage and positioned one aisle width (AISLE) upstage from the edge of the stage, which is also the y-axis. conference # - - - A p p . _ _ f u r n i s h def __furnish(self): '''Arrange the furniture in the scene. [ self.__scene is a Scene instance -> self.__scene +:= elements of the scene in their initial position ] ''' #-- 1 -# [ self.couch1 := as invariant # self.couch3 := as invariant # self.roundTable := as invariant New Mexico Tech Computer Center tkscene: A scene geometry manager 55 # self.leftChair := None ] self.couch1 = Couch(1) self.couch3 = Couch(3) self.roundTable = RoundTable(TABLE_RADIUS) self.leftChair = None #-- 2 -# [ self.tableCenter := center of the round table ] self.tableCenter = h.Pt ( STAGE_WIDE/2.0, AISLE+TABLE_RADIUS ) For the details of the furniture placements, see: • Section 22.9, “App.__placeTable()” (p. 56). • Section 22.10, “App.__placeCouch()” (p. 57). • Section 22.11, “App.__placeMovingChairs()” (p. 58). conference #-- 3 -# [ self.__scene +:= # self.__placeTable() a Table element centered at self.tableCenter ] #-- 4 -# [ self.__scene +:= a 3-seat Couch element facing the house # and centered on stage behind the table ] self.__placeCouch() #-- 5 -# [ self.__scene +:= two chairs (one-seat Couch elements) # facing the center of the table and at angles specified # by self.angleVar # self.leftChair := the left-hand chair element # self.rightChair := the right-hand chair element ] self.__placeMovingChairs() 22.9. App.__placeTable() conference # - - - A p p . _ _ p l a c e T a b l e def __placeTable(self): '''Place the table element into the scene. [ self.__scene as invariant -> self.__scene +:= a Table element centered at self.tableCenter ] ''' The table will be represented as a PlacedElt made from the Element instance self.roundTable. The transform used to place it into the scene is a single translation that moves the table's origin to self.tableCenter. 56 tkscene: A scene geometry manager New Mexico Tech Computer Center conference #-- 1 -# [ self.__scene := self.__scene with a new self.roundTable # element placed with its center at self.tableCenter ] PlacedElt ( self.__scene, self.roundTable, h.Xlate ( self.tableCenter.x(), self.tableCenter.y() ) ) 22.10. App.__placeCouch() First we instantiate a Couch element with three seats; see Section 22.14, “class Couch: Sectional seating” (p. 61). Then we use various transforms to move it into its final position on the stage. conference # - - - A p p . _ _ p l a c e C o u c h def __placeCouch(self): '''Place the couch into the scene. [ self.__scene as invariant -> self.__scene +:= self.couch3 placed behind the table, facing front, with COUCH_MARGIN clearance ] ''' The first step is to compose the various transforms needed to get the couch into the final position. In its reference orientation, the couch faces up, with its reference point at the southwest (lower left) corner. First we need to rotate the couch 180°around the reference point so that it points down. conference #-- 2 -# [ swing := an h.Xform that rotates 180 degrees ] swing = h.Xrotate(RAD_180) At this point the reference point is at the northeast corner. The next transform moves the reference point south to the south edge, then west to the center of the couch, half its length (Couch.COUCH_LONG). conference #-- 1 -# [ refShift := an h.Xform that translates the reference # point by (self.couch3.totalWide/2, Couch.COUCH_LONG) ] refShift = h.Xlate(-self.couch3.totalWide/2, -Couch.COUCH_LONG) Now that the couch is facing the house, the final transform shifts the reference point to the centerline of the stage, with its y coordinate equal to the table's center, plus the table's radius, plus COUCH_MARGIN. Finally, place the couch element according to the composition of all three of these transforms. conference #-- 3 -# [ placer := an h.Xform that translates x by # (self.tableCenter.x()) and translates y by # (self.tableCenter.y()+TABLE_RADIUS+COUCH_MARGIN) ] placer = h.Xlate ( self.tableCenter.x(), self.tableCenter.y() + TABLE_RADIUS + COUCH_MARGIN ) #-- 4 -# [ self.__scene New Mexico Tech Computer Center := self.__scene with a new self.couch3 tkscene: A scene geometry manager 57 # element placed according to the composition of # refShift, swing, and placer ] PlacedElt ( self.__scene, self.couch3, refShift.compose(swing).compose(placer) ) 22.11. App.__placeMovingChairs() This method retrieves the current value of self.angleVar and uses it to place two comfy chairs (oneseat Couch elements) so that they face the center of the table. For angle zero, the chairs face each other, their centerlines coinciding, all the way at the front of the stage (see the diagram in Section 22, “An example: conference” (p. 50)). As the angle increases, the chairs rotate around the center of thetable in the upstage direction, but the angle must not exceed MAX_CHAIR_ANGLE degrees. conference # - - - A p p . _ _ p l a c e M o v i n g C h a i r s def __placeMovingChairs(self): '''Reposition the two chairs that self.angleScale moves. [ self.__scene is as invariant -> if self.leftChair is None -> self.__scene +:= two chairs (one-seat Couch elements) facing the center of the table and at angles specified by self.angleVar self.leftChair := the left-hand chair element self.rightChair := the right-hand chair element else -> self.leftChair := self.leftChair moved to the position specified by self.angleVar self.rightChair := self.rightChair moved to the position specified by self.angleVar ] ''' Initially, we place the chairs using IDENTITY_XFORM (the identity transform) as a placeholder. The positions are then set by Section 22.12, “App.__setChairPositions()” (p. 59). conference #-- 1 -# [ self.__scene +:= two copies of self.chair1 placed at # any position # self.leftChair := one of those copies # self.rightChair := the other copy ] self.leftChair = PlacedElt ( self.__scene, self.couch1, IDENTITY_XFORM ) self.rightChair = PlacedElt ( self.__scene, self.couch1, IDENTITY_XFORM ) #-- 2 -# [ self.leftChair := self.leftChair repositioned by # rotating clockwise by the value of self.angleVar # relative to the base position # self.rightChair := self.rightChair repositioned by # rotating counterclockwise by the value of 58 tkscene: A scene geometry manager New Mexico Tech Computer Center # self.angleVar relative to the base position ] self.__setChairPositions() 22.12. App.__setChairPositions() conference # - - - A p p . _ _ s e t C h a i r P o s i t i o n s def __setChairPositions(self): '''Read the slider and position the left & right chairs [ (self.couch1 is as invariant) and (self.leftChair and self.rightChair are PlacedElt instance with element self.couch1) -> self.leftChair := self.leftChair repositioned by rotating clockwise by the value of self.angleVar relative to the base position self.rightChair := self.rightChair repositioned by rotating counterclockwise by the value of self.angleVar relative to the base position ] ''' The purpose of this method is to change the current position of the two moving chairs. The base position of these chairs, corresponding to angle 0° on the self.angleScale scale widget, is facing each other directly across the table, their centerlines parallel to the edge of the stage and passing through the center of the table. As the slider is moved, the left chair pivots around the table center in the clockwise direction, and the right chair counterclockwise. To reposition a PlacedElement, one replaces its .xform attribute with a homcoord.Xform instance representing the transform that moves the element from its reference space into the scene space. To simplify the logic of repositioning, we will develop the final transforms in two stages. 1. The first transform places a Couch(1) element in the position illustrated below: directly above the table, its centerline vertical and passing through self.tableCenter. This transform is local variable chairToTop. New Mexico Tech Computer Center tkscene: A scene geometry manager 59 y o COUCH_MARGIN 90 −θ o θ−90 TABLE_RADIUS θ x .tableCenter 2. The final transform that moves each chair from the position shown above is a rotation around the table center: by an angle of (90°-θ) for the left-hand chair, and by the negative of that angle for the right-hand chair. The first transform has two steps. First we rotate the chair 180° around the origin, so it is facing down, with its reference point at the top right corner. The rest is a translation: • The Δx component is the sum of: half the width of the couch, which moves the reference point to the vertical midline; and self.tableCenter.x(), which moves the reference point to the centerline of the stage. • The Δy component is the sum of: the length of the couch, which moves the reference point to the front edge of the couch; self.tableCenter.y(), which moves that point to the table center; and TABLE_RADIUS+COUCH_MARGIN, which moves that point to the position shown in the diagram above. conference #- 1 -# [ theta := value of self.angleVar in radians # rot180 := a homcoord.Xform that rotates 180 degrees # around the origin # shiftOrigin := a homcoord.Xform that translates x by # ((half the width of self.couch1) + # self.tableCenter.x()) and translates y by # ((the length of self.couch1) + (self.tableCenter.y()) + # TABLE_RADIUS + COUCH_MARGIN) ] theta = h.num.radians(self.angleVar.get()) rot180 = h.Xrotate(RAD_180) shiftOrigin = h.Xlate ( self.couch1.totalWide/2.0 + self.tableCenter.x(), self.couch1.COUCH_LONG + self.tableCenter.y() + TABLE_RADIUS + COUCH_MARGIN ) 60 tkscene: A scene geometry manager New Mexico Tech Computer Center #-- 2 -# [ chairToTop := composition of rot180, then shiftOrigin # leftSwing := a homcoord.Xform representing a rotation # of (90 degrees - theta) around self.tableCenter # rightSwing := a homcoord.Xform representing a rotation # of (theta - 90 degrees) around self.tableCenter ] chairToTop = rot180.compose(shiftOrigin) leftAngle = RAD_90 - theta leftSwing = h.Xrotaround ( self.tableCenter, leftAngle) rightSwing = h.Xrotaround ( self.tableCenter, - leftAngle ) #-- 3 -# [ self.leftChair.xform := composition of chairToTop # followed by leftSWing ] # self.rightChair.xform := composition of chairToTop # followed by rightSwing ] self.leftChair.xform = chairToTop.compose(leftSwing) self.rightChair.xform = chairToTop.compose(rightSwing) 22.13. App.__angleHandler(): Handler for the chair angle widget conference # - - - A p p . _ _ a n g l e H a n d l e r def __angleHandler(self, *p): '''Handle changes to the chair angle slider. ''' self.leftChair.erase() self.rightChair.erase() self.__setChairPositions() self.leftChair.tkDraw(self.can) self.rightChair.tkDraw(self.can) 22.14. class Couch: Sectional seating To reduce the number of different kinds of comfortable seating, we generalize easy chairs, love seats, three-seat sofas, four-seat sofas, and so forth into a single class representing seating for n people. Here is a diagram showing the rudiments and how they are rendered in a love seat (n=2). In this reference orientation, the arms are on the left and right, and the back is on the bottom. New Mexico Tech Computer Center tkscene: A scene geometry manager 61 y .COUCH_SEAT_WIDE .COUCH_ARM_WIDE A .COUCH_SEAT_LONG C C B A A D D .COUCH_BACK_LONG x A .__totalWide A The outline is rendered as a box rudiment with the four sides marked A. B This vertical represents the left-hand side of the right-hand arm. C Verticals are drawn on the left side of each person's seat. D These horizontal segments represent the front edge of the seat back. The scene dimensions are given as class constants, in inches. In the reference orientation, lengths (names including _LONG) are the vertical dimension. conference # - - - - - c l a s s C o u c h class Couch(Element): '''Sectional couch: n=1 for an armchair, n=2 for a love seat... Exports: Couch(n): [ n is a positive integer -> return a new Couch instance representing an armchair or section couch seating n people ] .n: [ as passed to constructor, read-only ] .totalWide: [ couch's total width ] ''' COUCH_SEAT_WIDE = feetInches(0,24) # Width of each person's seat. COUCH_ARM_WIDE = feetInches(0,2) # Width of the armrest. COUCH_BACK_LONG = feetInches(0,2) # Width of the back. COUCH_SEAT_LONG = feetInches(0,22) # Length of each person's seat. COUCH_COLOR = "#eeddbb" COUCH_BORDER_WIDTH = feetInches(0,0.5) #-# Total length of the couch. #-COUCH_LONG = COUCH_SEAT_LONG + COUCH_BACK_LONG 62 tkscene: A scene geometry manager New Mexico Tech Computer Center 22.15. Couch.__init__() conference # - - - C o u c h . _ _ i n i t _ _ def __init__ ( self, n ): '''Constructor. ''' #-- 1 -# [ self.totalWide := as invariant ] self.n = n self.totalWide = ( 2 * self.COUCH_ARM_WIDE + n * self.COUCH_SEAT_WIDE ) 22.16. Couch.render() Refer to the diagram in Section 22.14, “class Couch: Sectional seating” (p. 61) for the geometry. conference # - - - C o u c h . r e n d e r def render(self): '''Render the rudiments of self. ''' #-- 1 -# [ yield a box enclosing the entire couch, in the couch color ] yield BoxRudiment.make ( COUCH_TAG, Pt(0, 0), Pt(self.totalWide, self.COUCH_LONG), fill=self.COUCH_COLOR, width=self.COUCH_BORDER_WIDTH ) #-- 2 -# [ yield a line for the left side of the right armrest ] x = self.totalWide - self.COUCH_ARM_WIDE yield StraightRudiment.make ( COUCH_TAG, Pt(x, 0), Pt(x, self.COUCH_LONG) ) #-- 3 -# [ generate verticals and horizontals for each section ] for seatNo in range(0, self.n): xMin = self.COUCH_ARM_WIDE + seatNo * self.COUCH_SEAT_WIDE xMax = xMin + self.COUCH_SEAT_WIDE yield StraightRudiment.make ( COUCH_TAG, Pt(xMin, 0), Pt(xMin, self.COUCH_LONG) ) yield StraightRudiment.make ( COUCH_TAG, Pt(xMin, self.COUCH_BACK_LONG), Pt(xMax, self.COUCH_BACK_LONG) ) #-- 4 -raise StopIteration New Mexico Tech Computer Center tkscene: A scene geometry manager 63 22.17. class RoundTable This is an Element that renders as a filled circle of color TABLE_COLOR. The radius of the table is an argument to the constructor. The reference origin is the center of the table. conference # - - - - - c l a s s R o u n d T a b l e class RoundTable(Element): '''A round table Element. Exports: RoundTable(radius): [ radius is a radius in scene units -> return a new RoundTable instance with that radius ] .radius: [ as passed to constructor ] ''' 22.18. RoundTable.__init__() conference # - - - R o u n d T a b l e . _ _ i n i t _ _ def __init__(self, radius): '''Constructor. ''' self.radius = radius 22.19. RoundTable.render() The round table element renders a single element: a filled circle. conference # - - - R o u n d T a b l e . r e n d e r def render(self): '''Render self's single Oval rudiment. ''' #-- 1 -sw = Pt ( -self.radius, -self.radius ) ne = Pt ( self.radius, self.radius ) yield OvalRudiment.make ( TABLE_TAG, sw, ne, width=0, fill=TABLE_COLOR) #-- 2 -raise StopIteration 22.20. Epilogue conference # - - - - - 64 E p i l o g u e tkscene: A scene geometry manager New Mexico Tech Computer Center if __name__ == "__main__": main() New Mexico Tech Computer Center tkscene: A scene geometry manager 65 66 tkscene: A scene geometry manager New Mexico Tech Computer Center