CHAPTER 4 Structured Graphics

Draw Traversal


Suppose that you are using the Figgy program, and that you take your mouse and then click and drag one of the existing circles. In Figgy, the circle gets redrawn wherever your mouse moves, even before you release the mouse button. (We say the circle "tracks the mouse".) As typical with structured graphics, the circle is not redrawn directly. Instead, redrawing the circle involves a "draw traversal" of the entire glyph structure, beginning at the main viewer. This means that a Fresco GlyphTraversal object is created and recursively passed to every glyph in the structure. (Specifically, it is passed as an argument to each glyph's traverse() operation.) The GlyphTraversal's job is to maintain the "current traversal state" (such as the accumulation of graphical transformations) at each glyph during the traversal. Intermediate glyphs, such as viewers and the PolyFigure, pass the GlyphTraversal down to their children--the PolyFigure, after first concatenating its transformation matrix. Leaf glyphs (the circles and squares) respond to traverse() as you might expect--by drawing themselves on the display. Figure 4-5 shows the Glyph interface related to traversal and drawing, including the traverse() operation just discussed. Note that this operation takes the GlyphTraversal as its argument.

You might ask the following: Why traverse the entire structure at all when the circle is moved? Why not just erase the circle from its old position and re-draw it in its new position? Actually there are a couple of problems with this erase and redraw approach. First, if you dragged the circle across another drawable object (such as another circle), then that object would appear with "holes" at each position where the dragged circle was erased. (I.e., the lower circle would need some means of refreshing itself.) Second, you may wish to preserve "ordering" such that the circle could be dragged under an overlapping object. For example, you might want to drag an existing circle underneith a subsequently created rectangle. A simple erase and redraw approach would not allow this ordering to be preserved.

So, instead of using erase and redraw, Fresco uses a deferred update model, involving a full draw traversal from the main viewer. Now what about efficiency? Consider again the act of moving the circle and that the entire structure is traversed from the main viewer each time the circle is drawn in a new position (that is, each time a mouse position event comes in.) The fact is that it would be terribly inefficient, especially in a large drawing, to redraw all the leaf objects simply because we are moving a single circle. Fresco's deferred update model uses "damage-based" redisplay approach to prevent this.

When the circle is moved, its before and after positions on the display are "damaged". This means that Fresco's window object is notified that these regions will need to be redrawn. When the subsequent draw traversal occurs, glyphs are able to determine if their position intersects the accumulated damage region. If not, then these glyphs simply won't draw themselves. For viewers and composite glyphs such as the PolyFigure, this means that large branches of a given structure can be culled (pruned) during traversal.

Figure 4-6 shows how an implementation of PolyFigure might check to see if it intersects the damage area. This is done by calling the painter's is_visible() operation on line 9. If is_visible() returns false, then the PolyFigure's children are not traversed, and the branch is culled. (Don't worry if the code here doesn't make too much sense at this point; we'll refer back to it later with more explanation.)

Now that we've seen how the damage area is used for culling during traversal, lets see how the damage area gets set by the FiggyViewer. Figure 4-7 below shows the FiggyViewer code that "causes the damage" when the circle gets moved. Key are the two calls to the function need_redraw in lines 17 and 19.

The operation need_redraw is shown in the partial glyph interface of figure 4-5 above. Let's examine how calling need_redraw causes the damage. A typical implementation of need_redraw will call allocations(), also shown in figure 4-5, which causes a recursive upward traversal of the glyph structure and results in a list of AllocationInfos, each representing the accumulated traversal state along one trail to the given glyph. We'll examine trails in more detail later, but for now let's just say that--since glyphs can be shared--a trail represents a single instance of the glyph on the screen. Each AllocationInfo is then passed to the glyph's extension() operation which returns a region representing the actual screen real estate for the given trail. This region is used to extend the accumulated damage. So, in short, need_redraw, when called on a glyph, causes damage for each of that glyph's instances on the display.

But we said above that Figgy does not provide an Instance command and thus does not directly support the sharing of glyphs. Is calling allocations() (is determining screen real estate for multiple instances) then, really needed? The answer is yes. Although FiggyViewer does not support sharing directly, the fact is that the entire FiggyViewer can itself be instanced inside another viewer. This means that any single circle or square inside FiggyViewer can indeed appear multiple times on the screen.

Note on line 20 of figure 4-7 that glyph operation need_resize() is called after the circle's position has been changed. We do this because we must assume that some parent is storing size information about its children, and we need to notify such glyphs that the size of the children may have changed as a result of the move. The PolyFigure is a good example of a glyph storing such information. It stores a bounding box representing the area surrounding its children. It uses this during the traversal cull test shown in figure 4-6. When we translate the position of the circle, then the area surrounding the root PolyFigure's children may have just changed, so we call need_resize on the circle. Glyphs typically propagate need_resize up to their parents. When the PolyFigure receives the call to need_resize, it sets a flag indicating that its bounding box is out of date. This causes the call to update_bbox() in line 8 to re-compute the bounding box before the box is used for the cull test. The PolyFigure then calls need_resize on its parent(s), propagating the notification upwards.

Updating a bounding box is only one example of how glyphs define need_resize. Some glyphs assume that need_resize means their children's layout needs to be re-computed via a traversal. These glyphs force this (deferred) traversal by defining need_resize to cause damage around the area of their children.

Other glyphs prefer simply to stop the upward propagation of need_resize. Our FiggyViewer (figure 4-8 below) is a case in point. FiggyViewer re-defines the need_resize() operation as a nop, because its default (parent class) implementation would otherwise cause the call to propagate upwards.

void FiggyViewer::need_resize() { }
If the need_resize call were indeed allowed to propagate upwards, it would eventually make it up to the main viewer (shown in figure 4-2) which in turn would damage the entire window. But we are just moving a single circle! Damaging the entire window would cause every glyph inside the window to redraw during the traversal, because no culling could take place. (Culling requires localized damage areas.)

FiggyViewer prevents this by stopping the upward propagation. It knows that the circle will be damaged locally (via calling need_redraw in figure 4-7) and thus it need not depend on a parent glyph to do damage. Also, since the FiggyViewer's actual size does not change as a result of moving a circle, it need not be concerned with any parent's bounding box information requiring update.


Copyright (c) 1994 by Steve Churchill
Comments or questions? Contact Steve Churchill (stevec@faslab.com)

Generated with CERN WebMaker