Draw module specification

The current Draw module does not implement the floating point parts of this specification. Upgrading it to do so is a possible future development, but not a likely one.

Path format

A path is a sequence of one or more of the following path elements, each of which is a sequence of words:

  0 n                  = end of path, with n bytes remaining in buffer (n is
                           only important for output buffers, and will not be
                           read for input paths).
  1 ptr                = pointer to continuation of path.
  2 x y                = move to (x,y), starting new subpath. This subpath
                           will affect winding numbers and so is filled
                           normally.
  3 x y                = move to (x,y), starting new subpath. This subpath
                           does not affect winding numbers when filling (used
                           internally for thin-stroking paths). Note that any
                           subpath started by this element will always be
                           considered "closed" and so will never be closed by
                           the "close open subpaths" operation.
  4                    = close current subpath with a gap (c.f. element type
                           7).
  5                    = close current subpath with a line (c.f. element type
                           8).
6 x1 y1 x2 y2 x3 y3 = Bezier curve to (x3,y3), with control points at
                           (x1,y1) and (x2,y2);
  7 x y                = gap to (x,y), not starting new subpath - used
                           internally for the gaps in non-thickened dashed
                           lines.
  8 x y                = line to (x,y).

Only the bottom byte of the element type (i.e. the first word in each of the sequences above) is significant: the remaining three bytes are free for use in any way a client of the Draw module sees fit. (On output to the input path, the Draw module will leave these three bytes unchanged; on output to a standard output path, the Draw module will store zeroes in these three bytes.)

The element types noted as being used internally above can quite legitimately be used externally - the notes are simply to explain why these types exist at all.

Note that there are order constraints on these elements - in particular, element types 4 to 8 may not be used when there is no current subpath, element types 2 and 3 start a new current subpath (ending any previous subpath), and element types 4 and 5 end the current subpath.

Two lines, gaps or Bezier curves are considered to be adjacent if they both appear in the same subpath and one is defined immediately after the other, or if one of them is the first line, gap or Bezier curve of a subpath and the other closes that subpath (i.e. it is a path element type 4 or 5 and ends the subpath concerned). Adjacency of lines is used to decide whether to use a join to connect them when thickening a path.

A subpath is called closed if it ends with a path element of type 4 or 5 (so that its last line, gap or Bezier curve is adjacent to its first one); otherwise, it is called open. Note that a subpath which happens to end at the same point that it started is not closed unless it actually ends with a type 4 or 5 path element: this can make a difference to whether its first and last line segments are connected with a join or with two caps when the path is thickened.

The Draw module SWIs

All calls come in two versions, one a fixed point version and the other floating point. The former is faster but allows a smaller range of user co-ordinate systems and is sometimes less accurate.

For the fixed point versions, all numbers that relate to distances in the co-ordinate space (e.g. co-ordinates, line widths, etc.) are expressed in user-defined units before transformation. When the transformed path is output to the screen (in particular, when Draw_Fill or Draw_Stroke is called), the units after transformation are 1/256 of an OS unit (i.e. approx. 1/(256*180) inch = 1/46080 inch = 1/640 point). The default flatness is 512 (2 OS units), which produces reasonable results when output is to the screen and the identity matrix is used. All dimensionless quantities have 16 binary places (i.e. &10000 represents the constant 1.0).

Note that in the standard matrix:

      ( a  b  0 )
      ( c  d  0 )
      ( e  f  1 ),

a, b, c and d are dimensionless quantities (so have 16 binary places), while e and f represent a translation in the output co-ordinate space and so are expressed in output units (i.e. units after transformation). This varying interpretation of matrix elements is unpleasant, but necessary to retain reasonable behaviour (e.g. we want the inverses of all sensible matrices to be representable with reasonable accuracy).

The floating point versions use IEEE single precision floating point numbers in all circumstances. They are distinguished by the suffix FP on the name of the call. The default flatness is 2.0, for the same reason as above. Note: unless otherwise specified below, all co-ordinates are user
co-ordinates. PostScript concepts that are not redefined here should be interpreted as PostScript interprets them.

A further note: all calls use the colour (both pixel pattern and operation) set up for the OS VDU routines.

This means in particular that they can do things that PostScript cannot do - e.g. fill using operations such as AND, OR, EOR that pay attention to the current contents of the screen. Given this fact, I see no reason to restrict the module to PostScript's capabilities in other areas. This has resulted in the following additional non-PostScript abilities for the Draw module:

(a) Choice of fill style - e.g. fill including/excluding boundary, fill to

      halfway through boundary, fill exterior, etc.
(b) "Positive winding number" and "negative winding number" rules as well
      as PostScript's "non-zero winding number" and "even-odd winding number"
      rules.
(c) Line cap enhancements - particularly differing "leading" and "trailing"
      line caps and triangular line caps (together, these allow arrowed lines
      and similar effects).

Any program which wants to remain PostScript-compatible (which includes e.g. working with printer drivers) will of course have to avoid using these enhancements.

Draw_Fill, Draw_FillFP

These imitate the PostScript "fill" operator - i.e. they close open subpaths, flatten the path, transform it to standard co-ordinates, fix it if the call is Draw_FillFP and fill the result.

"Closing open subpaths" means adding a path element of type 5 ("close with line") to the end of any open subpaths in the path.

"Flattening" a path means bisecting any Bezier curves it contains recursively, at least until each of the resulting Bezier curves lies completely within a specified distance of the line segment joining its endpoints, and then replacing each such of the resulting Bezier curves by the line segment joining its endpoints. The specified distance is called the "flatness".

Entry: R0 points to the path.

        R1 contains the fill style:
             Bits 0,1:  0 = non-zero winding number rule;
                        1 = negative winding number rule;
                        2 = even-odd winding number rule;
                        3 = positive winding number rule;
             Bit 2:     Set if non-boundary exterior pixels are to be
                          plotted, clear if they are not;
             Bit 3:     Set if boundary exterior pixels are to be plotted,
                          clear if they are not;
             Bit 4:     Set if boundary interior pixels are to be plotted,
                          clear if they are not.
             Bit 5:     Set if non-boundary interior pixels are to be
                          plotted, clear if they are not;
             Bits 6-31 are reserved and should be zero.
           R1=0 is a special case. According to the above, it means "don't
           fill anything, non-zero rule" - a pretty useless operation. For
           convenience, it specifies a sensible default fill style, namely
           &30 (i.e. "fill to halfway through boundary, non-zero rule").
        R2 points to the matrix, held as 6 words in memory (in the order
           a,b,c,d,e,f for the matrix above); R2=0 means the identity
           matrix. Note that the code assumes that one user-defined unit is
           a small fraction of a pixel, and matrices that multiply
           distances by large factors may lead to inaccurate results.
        R3 contains the flatness, in user co-ordinates; R3=0 means default
           flatness.
Exit: R0 is corrupt (if V clear),
           or points to error block (if V set).
        R1-R11 preserved.

Some of the terms used above need defining. Briefly, pixels whose centres lie inside the path according to the winding number rule chosen are interior pixels, and pixels that would be plotted if the (flattened) path were plotted with thin line segments (i.e. 1 pixel wide line segments) are boundary pixels. If you are happy with these definitions, skip down to the section on Draw_Stroke. If you want more precise details, read on...

To determine whether a pixel is an "interior" pixel, calculate the winding number W of the path around the central point of the pixel. The pixel is then an interior pixel if:

Otherwise, the pixel is an exterior pixel.

Lines (including flattened Bezier curves) and gaps both contribute to the winding number calculation, provided the subpath that contains them started with an "move to" path element of type 2. Those in subpaths which started with a "move to" path element of type 3 are ignored.

To determine whether a pixel is a "boundary" pixel, consider the "diamond" that can be inscribed in the pixel by joining the centres of its sides. If a line (or part of a flattened Bezier curve, but not a gap) in the path passes through this "diamond", the pixel is a boundary pixel. Otherwise, it is a non-boundary pixel.

For the purposes of all the calculations above, regard the path as consisting of mathematical (i.e. infinitely thin) line segments. If e.g. a pixel centre or a vertex of a "diamond" lies precisely on the path, resolve the problem by regarding the path as lying very slightly to the right of its real position. If this doesn't help (because the line segment involved is exactly horizontal), regard it as lying very slightly above its real position.

        

Draw_Stroke, Draw_StrokeFP

These emulate the PostScript "stroke" operator - i.e. they:

(a) flatten the path;
(b) apply a dash pattern to the path; (c) thicken the path, using the current line joins and caps; (d) re-flatten the path if the specified thickness is non-zero; (e) transform the path to standard co-ordinates; (f) fix the path if the call is Draw_StrokeFP; (g) fill the resulting path.

"Applying a dash pattern to a path" means replacing the line segments it contains by an appropriate sequence of lines and dashes, determined from the dash pattern and the distance along the path from the start of the current line segment to the start of the line segment. It is not possible to apply a dash pattern to a path containing a Bezier curve.

"Thickening" a path means:

(a) If the thickness is zero, converting all "move to" path elements of

      type 2 to "move to" path elements of type 3, and otherwise leaving the
      path unchanged. Line caps and joins are ignored.
(b) If the thickness is non-zero, expanding each line segment in the path
      to a subpath defining a rectangle centred on the original line segment
      and of width equal to the specified thickness and expanding endpoints
      of line segments to subpaths defining appropriately sized and shaped
      caps and joins. Joins are used at common endpoints of two adjacent line
      segments. Caps are used at other endpoints, with the leading or
      trailing cap style being used depending on whether the endpoint
      concerned is a leading or trailing endpoint as the path is traversed.

It is not possible to thicken a path with non-zero thickness if it contains a Bezier curve.

Except in the special case of the line width being 0 (which means that as thin a path as possible is plotted, with line caps and joins being ignored), thickening the path is likely to result in a path that winds multiply around some pixels and/or in multiple plotting of some pixels. It is therefore inadvisable to use winding number rules other than the non-zero one, or non-idempotent colour operations (e.g. EOR). It is also inadvisable to try plotting the non-boundary exterior pixels. The on-screen results of doing any of these will probably not be what you expect!

Entry: R0 points to the path.

        R1 contains the fill style, as for Draw_Fill, except that R1=0 has a
           slightly different interpretation. If the line width is non-zero,
           it means &30 as above. If the line width is zero, it means &18
           (i.e. "fill boundary"), as the flattened and thickened path will
           in fact have no interior in this case! For zero width lines, the
           default fill style is usually the only sensible one.
             In addition, the top bit of R1 can be set to require the Draw
           module to plot the stroke all at once (i.e. using the "plot
           normally" option of Draw_ProcessPath rather than the "plot subpath
           by subpath" option). The main effects of this change are that the
           code will never double-plot a pixel, but also that it uses more
           workspace - possibly quite a lot more!
        R2 points to the matrix or contains 0, as for Draw_Fill.
        R3 contains the flatness or contains 0, as for Draw_Fill.
        R4 contains the line thickness, or 0 for a thin stroke (one pixel
           wide, with line caps and joins being ignored).
        R5 points to a line cap and join specification, which is a word-
           aligned four word area containing:
             Offset 0:  Byte 0 holds join style:
                          0 = mitered joins;
                          1 = round joins;
                          2 = bevelled joins.
                        Byte 1 holds leading cap style:
                          0 = butt caps;
                          1 = round caps;
                          2 = projecting square caps;
                          3 = triangular caps.
                        Byte 2 holds trailing cap style, with the same
                          allowed values as for leading caps.
                        Byte 3 is reserved and should be zero.
             Offset 4:  This holds a parameter associated with joins. For
                        mitered joins, this is the miter limit. It is not
                        used for the other join types. Note that the miter
                        limit is a dimensionless quantity and so (for
                        Draw_Stroke) is a fixed point number with 16 binary
                        places.
             Offset 8:  This holds a parameter associated with leading caps.
                        For triangular caps, this contains two 16 bit
                        unsigned numbers, each of which should be regarded as
                        having 8 binary places. The most significant part of
                        the number specifies how many line widths the cap
                        goes beyond the end of the line, and the least
                        significant part how many line widths it goes to the
                        side of the centre of the line. Thus e.g. &1000100
                        means a cap going 1 line width beyond the end of the
                        line and 1 line width to each side of its centre -
                        i.e. the cap width is twice the line width, and its
                        shape is a 45/90/45 degree triangle. This parameter
                        is not used for the other cap styles.
             Offset 12: This holds a similar parameter associated with
                        trailing caps.
           The module will make no attempt to read the last three words if
           the corresponding cap or join style does not use them, so it is
           legitimate to use a smaller area than four words if these
           parameters are not required.
        R6 points to a dash pattern, or contains 0 if an undashed line is
           required. A dash pattern must be word-aligned and has the
           following format:
             Offset 0:           Distance into the dash pattern to start.
             Offset 4:           Number N of elements in the dash pattern.
             Offsets 8 to 4N+4:  Elements of the dash pattern, each of which
                                 is a distance.
           The dash pattern starts with a dash of length equal to the first
           element above, then a gap of length equal to the second element
           above, etc. When the last element of the dash pattern has been
           used, the module starts again from the beginning. Note that if the
           number of elements of the dash pattern is odd, the distances will
           be interpreted differently the second time through, with the first
           element being a gap, the second a dash, etc.
Exit: R0 is corrupt (if V clear),
           or points to error block (if V set).
        R1-R11 preserved.

Draw_StrokePath, Draw_StrokePathFP

Entry: R0, R2-R6 set as for Draw_Stroke.

        R1 points to a word-aligned output buffer (which holds words 0, N,
        then N free bytes), or contains 0 to ask for the required output
        buffer size (i.e. N+8).

Exit: If V set, R0 points to an error block.

        If V clear and R1 was non-zero on entry, the output path has been
        inserted into the buffer, ending with an end of path indicator (i.e.
        words 0, N, where N is the number of free bytes following in the
        buffer). R0 points to this end of path indicator (and so is a
        suitable output buffer value for a subsequent path generating call
        that is to append to the path).
        If V clear and R1 was zero, R0 holds the required length.
        R1-R11 preserved.

This call puts the given path through all the stages done by Draw_Stroke or Draw_StrokeFP above, except the final fill. It then either saves the path that would be filled in a specified buffer or reports on how big a buffer is required, as requested.

Draw_FlattenPath, Draw_FlattenPathFP

Entry: R0 points to the input path.

        R1 points to a word-aligned output buffer (which holds words 0, N,
           then N free bytes), or contains 0 to ask for the required output
           buffer size (i.e. N+8).
        R2 contains the flatness, or 0 if the default flatness is to be used.

Exit: If V set, R0 points to an error block.

        If V clear and R1 was non-zero, the output path has been inserted
        into the buffer, ending with an end of path indicator (i.e. words 0,
        N, where N is the number of free bytes following in the buffer). R0
        points to this end of path indicator.
        If V clear and R1 was zero, R0 holds the required length.
        R1-R11 preserved.

This call flattens the given path. It then either saves the resulting path in a specified buffer or reports on how big a buffer is required, as requested.

Draw_TransformPath, Draw_TransformPathFP

Entry: R0 points to the input path.

        R1 points to a word-aligned output buffer (which holds words 0, N,
           then N free bytes), or contains 0 to transform the path in situ.
        R2 points to the matrix or contains 0, as for Draw_Fill.
        R3 is zero for fixed point output, &80000000 for floating point
           output. All other values are reserved.

Exit: If V set, R0 points to an error block.

        If V clear and R1 was 0, R0 is corrupt.
        If V clear and R1 pointed to an output buffer, the output path has
        been inserted into the buffer, ending with an end of path indicator
        (i.e. words 0, N, where N is the number of free bytes following in
        the buffer). R0 points to this end of path indicator.
        R1-R11 preserved.

This call puts the path through a transformation matrix and possibly fixes or floats it. It then either outputs the resulting path to a specified output buffer or puts it into the memory originally occupied by the input path. (Note that this call will never change the length of the path or indeed of any of its components. This last option is therefore safe - something which was not true for the two preceding calls.)

Draw_ProcessPath, Draw_ProcessPathFP

This allows a path to be put through a general set of the processes used when doing Stroke or Fill. One or more of the following can be requested, in the order given:

(a) close open subpaths;
(b) flatten the path;
(c) apply a dash pattern to the path; (d) thicken the path;
(e) re-flatten the path;
(f) transform the path by a matrix;
(g) fix a floating point path or float a fixed point one; (h) output the resulting path by one of six techniques:

      (i)   output to a specified output buffer;
      (ii)  count how long an output buffer is required for the given path
            and operations.
      (iii) fill the path normally - not legitimate for a floating point
            path;
      (iv)  fill the path subpath by subpath (this option saves workspace and
            can sometimes be used legitimately - for example, Draw_Stroke
            will often use it). Also not legitimate for a floating point path.
      (v)   output to the input buffer (i.e. modify the path in situ). Only
            valid if the path's length cannot be changed by the call - i.e.
            if steps (a) to (e) above are not requested.
      (vi)  output the path's bounding box, in transformed co-ordinates. This
            bounding box does not pay attention to pixel boundaries and is
            "inclusive" on all sides.

All combinations of these are allowed, other than the exceptions noted above. However, some are likely to produce errors (e.g. thickening a non-flattened path) and others are likely to produce strange results (e.g. filling a path containing open subpaths).

All the calls above (Draw_Fill, etc.) translate into specific calls to Draw_ProcessPath or Draw_ProcessPathFP: they are provided to ensure that suitable names exist for common operations and to reduce the number of registers requiring setting up and/or preserving.

Entry: R0 points to the input path.

        R1 contains a fill style in its bottom 6 bits, as for Draw_Fill,
           except that R1=0 has no special meaning. Other bits have the
           following meanings:
             Bits 6-26 are reserved and should be zero.
             Bit 27:  set if open subpaths are to be closed (step (a) above).
             Bit 28:  set if the path is to be flattened initially (step (b)
                        above).
             Bit 29:  set if the path is to be thickened (step (d) above).
             Bit 30:  set if path is to be re-flattened (step (e) above).
             Bit 31:  set if output path should be floating point, clear if
                        it should be fixed point (step (g) above).
        R2 points to a matrix, or contains 0 if no transformation by a matrix
           is to be done (step (f) above).
        R3 contains the flatness, or 0 for the default flatness (used in
           steps (b) and (e) above).
        R4 contains the line thickness (used in step (d) above). R5 points to
           a line join and cap specification (used in step (d) above).
        R6 points to a dash pattern, or contains 0 if the path is not to be
           dashed (step (c) above).
        R7 points to a word-aligned output buffer (which holds words 0, N,
           then N free bytes), or contains one of the following special
           values:
             0: output to the input path. This will only be accepted if the
                requested operations on the path are ones that cannot change
                its length - i.e. transforming the path by a matrix and
                fixing or floating it.
             1: fill the path normally.
             2: fill the path subpath by subpath.
             3: count the size of output buffer required.
             &80000000+address: output the bounding box of the processed path
                to the (word-aligned) address and the three following words,
                in the order lowX, lowY, highX, highY. Note: if this option
                is applied to an non-flat path, a valid but non-minimal
                bounding box will be produced.

Exit: R1-R11 preserved.

        If V set, R0 points to an error block.
        If V clear:
          If R7 was 0, 1 or 2 on entry, R0 is corrupt.
          If R7 was 3 on entry, R0 holds the size of output buffer required
            (i.e. N+8 if the output buffer should be words 0, N, followed
            by N free bytes).
          If R7 pointed to an output buffer, the output path will have been
            inserted into the buffer and R0 points to the new end of path
            indicator.

The Draw module vector DrawV

This provides a means of indirecting the calls above. Entry: R0-R7 hold values appropriate for the call concerned.
R8 specifies which call is involved:
0: Draw_ProcessPath
1: Draw_ProcessPathFP
2: Draw_Fill
3: Draw_FillFP
4: Draw_Stroke
5: Draw_StrokeFP
6: Draw_StrokePath
7: Draw_StrokePathFP
8: Draw_FlattenPath
9: Draw_FlattenPathFP
10: Draw_TransformPath

             11: Draw_TransformPathFP
Exit: R0 holds a suitable return value.
V set or clear as appropriate.
        R1-R11 corrupt.