tkscene: A scene geometry manager for Tkinter

advertisement
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
Download