a2dCanvasView and a2dDrawer2D
All rendering of a a2dCanvasDocument containing a2dCanvasObject's is achieved via a a2dCanvasView instance. a2dCanvasView in combination with a2dDrawer2D, can be seen as a high level Device Context, specially designed to work with the wxArt2D library. The a2dView called a2dCanvasView has a a2dDrawer2D which is used to do the real drawing. Specific a2dDrawer2D implementation do have a double buffering mechanisms. They all understand how to draw basic primitives in relative world coordinates. You can set fills and strokes, a drawstyle, clipping regions, mapping matrixes etc. It has several optimizations to speed up the drawing of primitives. Most important is that the conversion from relative world to absolute world, followed by conversion to device coordinates. This action is done with one matrix calculation. The a2dCanvasView itself administrates the updating of rectangular areas within a drawing in a sufisticated manner. The clue is, to first assemble areas or objects that require to be redrawn, and when the time is right, really do redraw those areas at once in an optimized manner. This makes a2dCanvas almost automatically redisplay modified objects or areas, without the user needing to bother about it. The modified object are in general part of the a2dCanvasDocument, but a2dCanvasView can have a a2dToolContr set for it. And this a2dToolContr can have tools which will draw object too. Therefor the whole update mechanism takes care of those objects inside tools also. A a2dDrawer2D is just a base class, the idea is to derive classes from it which can really draw something.
Mapping and zooming
The mapping to device coordinates and visa versa, is under control of the a2dDrawer2D class. a2dDrawer2D is a kind of wxDC, it allows to draw all basic primitives like polygons, polylines, circles Arcs etc. in world coordinates, but applying a certain transform matrix first to that primitive. Transformation to device coordinates and first relative to absolute coordinates is done here. This means that if you have a a2dCanvasObject which is a child of some other a2dCanvasObject, the a2dDrawer2D class will take care of drawing it at the right position in the a2dDrawer2D its device. a2dCanvasDocument is setup independent of the a2dCanvasView and the a2dDrawer2D that is used to display the data/drawing contained within the a2dCanvasDocument. Internally a2dCanvasDocument only uses a a2dDrawer2D base pointer to display the objects. Any a2dDrawer2D derived class can be used to display the drawing contained within the document to a device. A a2dCanvasObject uses a a2dDrawer2D to draw itself by drawing basic primitives, it first sets the accumulated matrix to the a2dDrawer2D. This matrix takes care of transforming the basic primitives that represent the object from its relative position (towards its current parent object within the drawing) to an/the absolute position. One a2dCanvasObject can be placed several times within a drawing, references to the object are used to do this. This means that an object does not have a unique absolute position within a drawing, and is defined only towards its parent object's. The accumulated matrix is calculated when recursively rendering a document. The accumulated matrix includes all transforms on the path to a certain a2dCanvasObject. It is used to calculate the absolute coordinates which a a2dCanvasObject at that position/path in the document has. The absolute coordinates are calculated within the a2dDrawer2D, and in there combined in one matrix to directly go to the device coordinates also. Before really rendering an object, first the accumulated matrix is set for the a2dDrawer2D that is used to render the document. This matrix is directly combined with a matrix which converts to device coordinates, the resulting matrix will be used to convert all primitives vertexes which will be drawn to an absolute position. Because the implementation of the drawing routines inside a a2dDrawer2D are polygon based. Every basic primitive is converted to a polygon/polyline or set of polygons/polylines, the vertexes of those polygons will be converted to absolute and device coordinates. The resulting polygons will be rendered. This way we can have transformed circles, ellipses and arcs etc. So a relative drawn circle ( drawn by a a2dCanvasObject) can eventually be drawn like an rotated ellipse. This would have bin impossible with just a wxDC based implementation.
There is a a2dDcDrawer for printing a a2dCanvasDocument, which is used by a2dViewPrintout to print a a2dCanvasDocument. It is possible to develop a a2dOpenGlCanvas and a a2dOpenGlDrawer. Since OpenGl will display 3D objects normally, in this case the a2dCanvasDocument layers can have thickness, so the result can be really 3D in a way. All objects drawn on a layer will get the same "z" coordinate based on the order the layers are drawn and the thickness of each layer.
Special antialiased libraries do exist that can be used to draw into bitmaps, allowing alpha blending/compositing. The basic Fill and Stroke objects have a opacity member which is used to define the transparancy of the drawn primitives. Writing a a2dDrawer2D based on those libraries, opens a new range possibilities, like new types of Strokes and Fills one can use. The current best implemented library is the Antigrain Library.
A a2dDrawer2D derived class like a2dMemDcDrawer is used to do the drawing in world coordinates in a wxDc based fashion, but it does it double buffered. Internal it draws first to a bitmap buffer, which is later, in idle time, blitted to the window. The same a2dMemDcDrawer is used internal in a2dRenderImage. Here it takes care of rendering a a2dCanvasDocument to a bitmap. A a2dRenderImage is a a2dCanvasObject itself, and can be made part of a document like any other a2dCanvasObject. It uses the a2dDrawer2D its bitmap for the internal image to display it self as a a2dCanvasObject. So indeed a2dRenderImage will be part of a a2dCanvasDocument structure in the end. The effect is that you can have a canvas which displays a2dRenderImages which contain data that you can/would/could normally display on a a2dDrawer2D directly. As such it can be used in browser like programs to display one or more vector drawings on specific parts of the window. In a browser this might be used to display SVG content.
Printing a a2dCanvasView View
Printing part of a a2dCanvasDocument is seen as printing a view of the document. a2dViewPrintout is used to simplify this task. The a2dCanvasView is used by the printing classes to extract the zoom of the view, and internal a a2dDcDrawer is used to render the document to the printer. One can also print the whole document at once, and not only that part which is displayed on a view. For this there are two types of print, one for the view and one for the document (wxID_PRINT_VIEW wxID_PRINT_DOCUMENT). The following list shows some ways to draw/print a a2dCanvasDocument recursively while printing:
a2dViewPrintout#render a canvas document through this class which is the drawer its a2dCanvasDocument
a2dCanvasDocument #render this document that contains a2dCanvasObjects
a2dCanvasObject #render this object
- printer #here is it drawn to and contains end result
Double buffer and updating
Some a2dDrawer2D's do have a backup buffer, that is used to render into. For the a2dMemDcDrawer this is a bitmap that is drawn into. It depends on the implementation of a2dDrawer2D derived classes how things are implemented. When used, the backup buffer is blitted to the real device when the rendering is finished. a2dMemDcDrawer uses wxMemoryDc to draw into the buffer.
a2dDrawer2D has functions to draw basic primitives to a device, this device can be a buffer or a window are any other rectangular area. The primitive drawing functions are used inside a2dCanvasObject's to draw the object, and they are independent of the real device. This way the same a2dCanvasObject's can be displayed using different a2dDrawer2Ds.
a2dCanvasView's do have a a2dCanvasDocument set for it, and one a2dCanvasObject from the document is set as the ShowObject. This object and all its nested children are displayed on the view. When a change in the document is detected, the a2dCanvasDocument will make sure that all a2dCanvasView's using the document will be updated.
a2dCanvas has a a2dCanvasView, and this last class uses internal a a2dMemDcDrawer with a bitmap having the same size as the client window. When rendering is complete the resulting bitmap is blitted to the a2dCanvas window. This is done in Idle time, or when wanted by the user. The a2dDrawer2D its bitmap buffer is also used to quickly repaint damaged parts caused by overlapping dialogs and other windows. But this task is part of a2dCanvasView. In those cases it will not render those parts again, only a simple blit is enough. Real re-rendering takes place when the mapping changes due to zooming in, scrolling, or resizing a window, and of course when the data in the document itself changes. This last one means re-rendering only those parts in the a2dCanvasDocument that really did change.
When rendering the graphical data which is stored inside a a2dCanvasDocument as a2dCanvasObjects, all the a2dCanvasView using the document will be updated one by one. When updating and rendering part of a document to a view, the iteration context a2dIterC is used as a parameter to the rendering functions. The a2dIterC contains a pointer to the a2dCanvasView from where the renderings started. Via this a2dCanvasView, it has also access to the a2dDrawer2D for that a2dCanvasView.
All a2dCanvasObjects stored within a a2dCanvasDocument (nested also), have a pointer to the a2dCanvasDocument object to which they belong. This m_root pointer is used to notify the views on the document that there are pending objects. The document is check for changes in idle time, and sents update event to all views. This then result in the views bringing themselfs up to date. The a2dCanvasView itself knows the width and height of the device to render to. It might be a Window but also a bitmap being part of a window or some other type of device for which there is a a2dDrawer2D available. It is very simular to wxDC in wxWidgets, only this one is dedicated to a2dCanvas and alows to draw directly in world coordinates with a given placement matrix. a2dCanvasView is a a2dView derived class, and knows the a2dCanvasDocument it needs to view..
The same a2dCanvasDocument can be displayed with/on N a2dCanvasView objects at the same time. And all of them can display a different rectangular part of the same top a2dCanvasObject. In case of nested data, the ShowObject which is displayed within all of the a2dCanvasView instances, can be different too. Updating objects that have changed in position or size etc. will be properly updated on all a2dCanvasView objects. This is under control of a2dCanvasDocument. When an object is changed, it sets a pending flag to indicate that, at the same time a flag is set in the a2dCanvasDocument. This flag is checked in idle time, so the document knows when there are changed objects. When there are changed objects, the document sents an update event to the central event distributer a2dEventDistributer or a special set distributer class (e.g a a2dCanvasView in a single a2dCanvas situation). This class then sents the event to all a2dView's which are registrated to it. Because a2dCanvasView is derived from a2dView, it will get the update events. The a2dView now checks if the update event is sent from the document that it is meant to display, and if so starts updating its view. For a a2dCanvasView this means that the a2dCanvasDocument is traversed and all boundingboxes of the changed objects are added to the redraw list of the a2dCanvasView. The object that changed did not yet update their boundingbox, and therefore they still represent the boundingbox of the object before it was changed. As soon as all a2dCanvasView's have assembled the boundingboxes of the previous state, a2dCanvasDocument recalculates the boundingboxes of the changed objects, and all that depend on it. Next it sent another update event, and this result once more in the a2dCanvasViews adding update areas of the new boundingboxes of the changed objects in the document. In the end all drawers have assembled all the areas changed in its view, all those areas will be redrawn. So when an object changes all it needs to do is report to the a2dCanvasDocument that it did change, which automatically results in updating the object on the a2dCanvasView devices.
There or two ways to draw a a2dCanvasObject using a a2dDrawer2D. The direct way is to use the basic drawing features like drawpolygon, drawcircle etc., they draw the polygon or circle defined in relative world coordinates. Here the drawer does all the conversion from relative to absolute world coordinates (using the supplied matrix) and later to device coordinates. Clipping those polygons can be done in the drawer itself, first at the worldcoordinate level and later on device coordinate level. It uses the clippingregion set for the active drawer to do this. The advantage of this method of drawing is that many things like transformation and filling style routines can be concentrated in the drawer. Arrays containing converted device coordinates, can make use of one and the same array within a2dDrawer2D. This type of cashing makes drawing much faster.
Another method of drawing is to first ask the drawer for a part of the a2dDrawer2D bitmap buffer. This bitmap will have the size of the clippingbox that is set to the a2dDrawer2D when re-rendering an area. The canvasobject to draw, will retrieve that area from the backup buffer in a2dDrawer2D, and add to it what it wants. The job of the a2dCanvasObject its RenderFunction is to fill this bitmap in some way. Clearly the bitmap to fill is in device coordinates and therefore all transformations and conversion to device coordinates needs to be done within the render function itself. Still this can be the best way in cases where data needs to be cashed within the object in order to get acceptable speed. When the bitmap is filled it needs to be drawn back to the drawer its total bitmap buffer. This method allows cashing within the a2dCanvasObject itself. For example, the a2dImage, preserves the scalled image/bitmap. This cashed bitmap is used during redraws as long as the object itself or the mapping to device coordinates does not change. Of course very good for speed, bad for memory usage.
Updating the a2dCanvasView or a2dCanvas window
Updating means, redrawing/rerendering parts of a drawing that is stored within a a2dCanvasDocument, and it depends on the implementation of the a2dCanvasView how this is done. When using a a2dMemDcDrawer for drawing, it renders and stores the result into a buffer first, and uses this buffer for quick blits to the real device, which is the a2dCanvas window. This is called double buffering, and down here this type of a2dDrawer2D is used for a2dCanvasView. Each a2dCanvasView displays part of the total drawing as set by its mapping. When the displayed part has changed, an update of this part is needed to reflect the changes on the a2dCanvasView its device and buffer. But also an update is needed when objects in the drawing change. The several a2dCanvasView's on a a2dCanvasDocument which do display the changed objects will be partly updated in that case.
Requesting updating of an area
Requesting repainting areas of a drawing is done by a2dCanvasView::AddPendingUpdate members. Repainting the areas of all a2dCanvasView's which display a certain a2dCanvasObject, is done via the a2dCanvasDocument instance to which that a2dCanvasObject belongs.
A first way of requesting an area to update, is adding a rectangle to the updatelist of a a2dCanvasView. A second way is to give a pointer to a a2dCanvasObject The area currently occupied by the object will be added to the updatelist of the a2dCanvasView. Those two methods make the programmer responsible for supplying the correct areas for updating on every a2dCanvasView. Often this method is used for interactive tools which directly manipulate the data in one a2dCanvasView view. Only in the end all other a2dCanvasView's displaying the object are updated also. A third way of requesting updates is automatically arranged by the wxArt2D library. It requires to mark an object as pending, and everything will be taken care of. This is happening from within the a2dCanvasDocument, but it still uses a2dCanvasView::AddPendingUpdate to add areas to the updatelist within all a2dCanvasView's of the document.
Updatelist within a2dCanvasView
Whatever method, they all eventually use the updatelist of every a2dCanvasView. The updatelist is in principle a list of rectangle's. Each update area has a two flag's. One to check if the area requested for update is already redrawn to the buffer and the other to check if that area is blitted to the screen also. The two action can be separated in time. When pending updates areas are added to the list within a a2dCanvasView, they are not directly updated/redrawn in the screen buffer, but only when idle, or in Onpaint. As soon as the buffer contains the redrawn areas, they can be blitted to the screen also. It depends on the action in progress if this is done directly or delayed. a2dCanvasView::RedrawPendingUpdateAreas() updates/redraws the areas in the updatelist to the buffer, flagging them for blit later. a2dCanvasView::BlitPendingUpdateAreas( wxDC & dc ) blits the areas that were updated to the screen in Onpaint or Onidle, or when needed. The blitted areas or directly deleted, resulting in having an empty updatelist.
In some cases it is better to first redraw all update areas to the buffer, and delay the blitting to later. For instance while scrolling the window, where first the buffer is brought up to date, and the blitting to the screen is handled by shifting the whole buffer followed by a blit. So in those cases blitting the damaged parts is not needed. In the normal case the areas in the updatelist will be redrawn and the redrawn parts in the buffer will be blitted to the window in idle time, or when possible in Onpaint after a paint event. When adding areas to the updatelist, the new area is checked against the areas already there, and if there is overlap the two areas will be combined. This in the end means that there will be less areas to redraw.
The third way of requesting an area to update, sets a a2dCanvasObject it's pending flag and at the same time informs the a2dCanvasDocument that an object is pending for update. Many a2dCanvasObjects may be set pending at once. As soon as the program is Idle the Onidle member of the a2dCanvas's gets called. Here, via the a2dCanvasView of a2dCanvas, the a2dCanvasDocument is checked for the pending a2dCanvasObject's flag. If the document has pending objects, the real updating will take place. The a2dCanvasDocument is traversed, searching for pending objects, and the previous and the current boundingbox of the object are added to each a2dCanvasView updatelist at all positions the object is placed (references included). All paths leading to the object will result in two rectangles being added to the updatelist of the a2dCanvasView's. Since object area's can depend on the a2dCanvasView itself, in case of pixel extended objects, and also the object shown one each drawer may differ, the above is done for each a2dCanvasView seperately. When all a2dCanvasView updatelist's do contain the areas to update, those areas will be redrawn. Several areas may need an update, which means traversing the a2dCanvasDocument several times, in order to rerender each rectangular area. Therefore it is important to minimize the number of areas to update. Updating an object is inclusive references higher or lower in the datatree. Updating is initiated from a2dCanvasDocument. a2dCanvasDocument sents an update event as explained in Double buffer and updating. a2dCanvasObject update areas (defined in pixels) can only be specified for a certain a2dCanvasView and only as seen from the current shown showobject in that view. This is because the mapping of the view/drawer and the a2dCanvasObject shown on each a2dCanvasView can be different. The area occupied in pixels in a certain a2dCanvasView can be different for the same object displayed on another a2dCanvasView. a2dCanvasView::AddPending reports areas to be updated in world or device coordinates to the updatelist, the list itself is always in device/pixel coordinates. In case of world coordinates, it takes into account the a2dCanvasView its mapping to calculate the update area in device coordinates. The absolute position of the changed object its boundingbox as seen from top object shown on that a2dCanvasView is added to the update list. A a2dCanvasObject can have more then one parent, if it is referenced more then once. Therefore the same relative object is displayed at several absolute locations. If such an object changes, several areas on the top level may need an update, since the same object is placed at several locations. Often the update areas are based on the relative boundingbox of objects. If an object has the pending flag set, the old boundingbox of the object, which was the area occupied before the change to the object was made, is added first. This is done for all pending objects in the whole document as seen from the seperate a2dCanvasView's. The result is that all the absolute boundingboxes of all references to the object are added to the updatelist. After that the new boundingboxes are calculated for all pending objects, those new object areas are also added to the update list by traversing the a2dCanvasDocument once more. In the end all pending flag inside objects are reset.
The complete render cycle, according to the following list of events:
- Change several objects
- set pending flags in changed objects and in the document
- In Idle time or in Onpaint , the document pending flag triggers updating for all views.
While traversing, the accumulated matrix is used to calculate absolute boundingbox from the relative boundingbox of pending objects. This OLD boundingbox is added to the updatelist of the current a2dCanvasView.
The a2dCanvasDocument starts traversing the document to recalculate the relative boundingboxes of pending objects.
The a2dCanvasDocument starts traversing the document via each a2dCanvasView. Traversing in each a2dCanvasView starts at ShowObject. While traversing, the accumulated matrix is used to calculate absolute boundingbox from the relative boundingbox of pending objects. This time based on the NEW boundingbox that was calculated in the previous step. This boundingbox is added to the updatelist of the current a2dCanvasView.
- document resets all pending object flag in the document itself and pending objects.
In idle time or Onpaint EACH a2dCanvasView sets itself as active drawer for the a2dCanvasDocument it belongs to. The a2dCanvasView renders each area in its update list, relative from its showobject within the a2dCanvasDocument it needs to display. a2dCanvasView its a2dMemDcDrawer uses a wxMemoryDc to draw into a bitmap buffer. Only objects overlapping or within a specific area are re-rendered. Overlapping areas are clipped to the area.
Updating All drawer or one
The area to update is always calculated for the current ShowObject in a certain a2dCanvasView. One object can be shown on several a2dCanvasView objects, but on a separate child level and also other references may exist in the same document. Therefore it is always necessary to update all areas occupied on all the a2dCanvasView/a2dCanvas objects. Still it is up to the programmer to decide when to do it, taking into account speed etc. To give an example: when dragging an object using the mouse on a certain canvas, you may decide to only show the effect on the other non active canvas's when the dragging is finished. The programmer will freeze the automatic update mechanism until the dragging is finished. During the dragging action no a2dCanvasView will be updated as a result of pending objects. As soon as the drag tool releases the freeze setting, all changed objects will be updated. Still the programmer is responsible for adding the old boundingboxes to the updatelist's, before the drag changed the object. In special cases the updating of references is delayed.
Combining update areas
As soon as the application becomes Idle, it will start Redrawing all areas specified in the updatelist's of the a2dCanvasView's. Several pending updates can be added before real redrawing/updating takes place. Changing or adding many a2dCanvasObject's in a row, which all had the pending flag set, will only result in one redraw action as soon as the application becomes idle. Also this redraw actions will be combined in as little redraw areas as possible, by combining redraw areas into one. Adding areas to the Updatelist of a a2dCanvasView is optimized, overlapping area's etc. are combined using filled tile rectangles. It is important to reduce the number of update rectangles, since each rectangle means render that area. Rendering means traversing the document, if areas do overlap the object in the overlapping part would be drawn twice. So i that case it is better to combine them. But combining two areas may also result in more things to be redrawn then is strictly needed. Think of L shaped structures.
A technique to improve updating a bit more is called "micro tiling". This work as follows. The total area to draw on a a2dCanvasView is divided in small tiles. An object to be updated has, or generates a list of rectangles to be updated. Those rectangles overlap certain tiles and only those tiles will be flagged for update. In the end rendering the tiles needing an update will always be limited to the number of total tiles available. On top of this, it is possible to sub divide tiles. For instance in a tile is defined by a 4 byte long. This long contains the 8 bit integers to address the maximum and minimum x and y of a bounding box within the tile. The bounding box inside the tile is extended when an object overlaps that tile. And of course in such a manner that only the overlapping/intersecting part is reflected in the small bounding box of the tile. Again in the end updating would be limited to the maximum of tyles, but now the tiles to update will not always need to be totally redrawn. Only the part defined by the small bounding box inside the tile is really updated. The difficult part is calculating which and how tiles do overlap a a2dCanvasObject which needs an update. For polygon and other filled shapes this means scanning with a certain grid. For polylines and other non filled shapes the trick is to divide the outline into segments. The segments are checked for overlap with tiles, taking into account thickness. Currently a2dCanvasObject only reports the rectangular redraw area before and after it was changed. When a document has reported all changed rectangular areas, to its views, the views will be repainted. It takes the array of filled tiles, and first starts to combine as much connected tiles which need to be (partly) redrawn into bigger rectangles. The end result is a list of rectangles, which represent all the areas which must be redrawn.
TODO Drawing of tile approach with a polygon divided over tiles in a a2dCanvasView.
Clipping to redraw areas
The clipping of a2dCanvasObject to the rectangle to update/redraw, is another optimization. At this moment the API of wxWidgets is used to this, but in some cases it is better to do a pre-clip, in order to limit the number of things to transform and re-draw. Think of a polyline that has only one segment overlapping the update rectangle, and 1000 segments outside it. Clearly it is better to first reject the 1000 and only draw the one that overlaps.
TODO Drawing of huge polyline, with only very small part displayed on a2dCanvasView.