C for Yourself - part 52

Steve Mumford picks a path through the machinations of the Draw module

Following on from the article two months ago, this time round I'll take a closer look at some of the mechanics behind the Draw module. We know in essence what it's capable of - a simplified implementation of a Postscript-style drawing technique, the Draw module allows you to define and manipulate objects known as paths. These paths consist of straight lines, Bezier curves and move instruc- tions and many different parameters can be changed to affect their appearance - dotted and dashed lines are possible and the joins and ends of lines are also customisable.

In the May issue I introduced the range of different units that are utilised by the Draw module and mentioned two of the major SWI calls provided, Draw_Fill and Draw_Stroke. In fact, these are two offspring of a much more versatile function, Draw_ProcessPath, and have been provided so simpler operations can be performed without having to pass a whole load of redundant parameters to the parent function.

These SWIs operate on the same data structures; there are several that work together to fully define a path, including details of the path co-ordinates themselves, the fill style, the dash pattern and the path's caps and joins. We've already met the transformation matrix that's used to scale, rotate and position the resultant path.

The paths themselves are built up of subpaths and those are built up of separate units called elements. The best way to imagine this is to picture a screen with a straight line, a rectangle and a curve displayed upon it. All three shapes could be part of the same path (with 'move' instructions between each one) but each would exist as a discrete subpath.

The objects themselves consist of differing numbers of elements - the straight line could be described as a 'move' instruction followed by a 'draw' command, and the rectangle would just consist of one 'move' followed with four 'draw' orders. The curve is similar to the straight line although each segment would require more parameters, as the co-ordinates of the line's control points have to be stored as well.

So how are these paths stored in memory? The format is surprisingly simple and consists of a long block of words with a special terminator instruction at the end of the list. Each element is at least one word in length and this first word contains the element type (currently in the range 0 to 8). Most of the other elements you can use require parameters, whether they happen to be a pair of words giving the x and y co-ordinates for a move or draw instruction or a one-word address pointing to another memory block that holds the remainder of the Draw path.

Consider the following memory block - it contains four elements and draws a single straight line from (5000, 5000) to (7000, 8000) in 'user co-ordinates', which are converted to Internal Draw units by placing appropriate scale values in the transform matrix.

0:    2
+4:   5000
+8:   5000     (move to 5000, 5000)
+12:  8
+16:  7000
+20:  8000     (draw to 7000, 8000)
+24:  4        (close current subpath)
+28:  0
+32:  0        (close path)

Once a path has been stored in memory, you're free to plot it to screen, printer or memory using one of the supplied SWIs. Draw_Stroke renders a path and sends it to the VDU, changing the line style where necessary, adding caps and joins and 'flattening' the path so the curves displayed on screen are a good approximation to their mathematical descriptions held in memory. Draw_Fill does the same, but fills in any closed subpaths as it goes. Both commands will use the currently selected VDU colour and neither will plot outside the current graphics window.

Although Draw_Stroke takes values in registers R0 to R6, most of these are optional or provide defaults and the only crucial ones are the pointers to the Draw path itself in R0 and to the transformation matrix in R2. R1 holds the fill style, a word whose bits determine which winding rule is used, and whether boundary pixels are filled, but supplying a value of 0 indicates a standard fill is to be used.

The same goes for the flatness and line thickness in registers R3 and R4; a flatness value of zero asks the Draw module to make a sensible guess given the current screen resolution and a thickness value of zero indicates that the lines should be as plotted as thinly as possible. 'Thin' lines don't need cap or join information; if this was required, a pointer would be stored in R5. Draw_Fill is almost the same, although it only accepts values in registers R0 to R3 since you can't specify line thickness or style with this SWI.

That's all for this month. I hope the material in the last few issues has provided you with the information to be able to plot and manipulate Draw paths and files in your own programs. I'll be utilising these techniques next month when I start putting the framework of our label database together.

Source: Acorn User - 196 - July 1998
Publication: Acorn User
Contributor: Steve Mumford