This is I think is of general interest, although motivated by work on Xpost.
I want to implement in C a data structure for PS paths.
Here's the version from the draft interpreter xpost0c, before I rewrote it to use Cairo for graphics. This isn't my earliest version because there was something in flips that make Xlib calls, but I'm not finding any copies of that at the moment.
These are xpost0c's operator functions. They receive a pointer to a C-array of the object arguments from the ps operand stack, and place their return values into the same array (which is, in fact part of the ps operand stack directly accessed by the pointer).
/* path construction operators */
typedef struct s_path {
enum e_path { MOVE, LINE, CURVE, CLOSE } type;
double v[6];
} Path;
void Onewpath (Object *o);
void Omoveto (Object *o);
void Olineto (Object *o);
void Ormoveto (Object *o);
void Orlineto (Object *o);
void Ocurveto (Object *o);
void Oclosepath (Object *o);
#include "global.h"
/* - newpath -
initialize current path to be empty
*/
void Onewpath (Object *o) {
tailpath = subpath = curpath;
}
/* x y moveto -
set current point to (x, y)
*/
void Omoveto (Object *o) {
promote(o[0]);
promote(o[1]);
if (!typearg2(real,real)) error(typecheck, OP_moveto);
if (tailpath == NULL) tailpath = curpath;
subpath=tailpath;
*tailpath++ =
(Path){
.type=MOVE,
.v[0] = applyCTMx(o[0].r,o[1].r),
.v[1] = applyCTMy(o[0].r,o[1].r),
};
cp[0] = o[0].r;
cp[1] = o[1].r;
}
/* dx dy rmoveto -
relative moveto
*/
void Ormoveto (Object *o) {
promote(o[0]);
promote(o[1]);
if (!typearg2(real,real)) error(typecheck, OP_rmoveto);
o[0].r += cp[0];
o[1].r += cp[1];
subpath=tailpath;
*tailpath++ =
(Path){
.type=MOVE,
.v[0] = applyCTMx(o[0].r,o[1].r),
.v[1] = applyCTMy(o[0].r,o[1].r),
};
cp[0] = o[0].r;
cp[1] = o[1].r;
}
/* x y lineto -
append straight line to (x, y)
*/
void Olineto (Object *o) {
promote(o[0]);
promote(o[1]);
if (!typearg2(real,real)) error(typecheck, OP_lineto);
*tailpath++ =
(Path){
.type=LINE,
.v[0] = applyCTMx(o[0].r,o[1].r),
.v[1] = applyCTMy(o[0].r,o[1].r),
};
cp[0] = o[0].r;
cp[1] = o[1].r;
}
/* dx dy rlineto -
relative lineto
*/
void Orlineto (Object *o) {
promote(o[0]);
promote(o[1]);
if (!typearg2(real,real)) error(typecheck, OP_rlineto);
o[0].r += cp[0];
o[1].r += cp[1];
*tailpath++ =
(Path){
.type=LINE,
.v[0] = applyCTMx(o[0].r,o[1].r),
.v[1] = applyCTMy(o[0].r,o[1].r),
};
cp[0] = o[0].r;
cp[1] = o[1].r;
}
/* x_1 y_1 x_2 y_2 x_3 y_3 curveto -
append Bexier cubic section
*/
void Ocurveto (Object *o) {
*tailpath++ =
(Path){
.type=CURVE,
.v[0] = applyCTMx(o[0].r,o[1].r),
.v[1] = applyCTMy(o[0].r,o[1].r),
.v[2] = applyCTMx(o[2].r,o[3].r),
.v[3] = applyCTMy(o[2].r,o[3].r),
.v[4] = applyCTMx(o[4].r,o[5].r),
.v[5] = applyCTMy(o[4].r,o[5].r),
};
cp[0] = o[4].r;
cp[1] = o[5].r;
}
/* - closepath -
connect subpath back to its starting point
*/
void Oclosepath (Object *o) {
*tailpath++ = (Path){ .type=CLOSE, .v[0] = subpath-curpath };
}
Hm. Looks like closing segments may have been having y-coordinate problems. :)
So this code just uses a global array (curpath) and a pointer (tailpath) and an auxiliary pointer (subpath) into an array of structs containing an enum and up to 6 coordinate values.
My current code implements the path entirely in postscript, using dictionaries for almost everything due to their nice properties: named elements, auto-growing, support for integer keys.
I won't quote everything here, you can browse it at
code.google.com/p/xpost in data/
path.ps.
% path == <<
% 0 << subpath0 >>
% 1 << %subpath1
% 0 << elem0 /move >> %first elem must be /move
% 1 << elem1 >>
% >>
% >>
% A /move element will always start a new subpath
% Any other element appends to the last subpath
% Path Construction Operators
% - newpath -
% initialize current path to be empty
/newpath {
graphicsdict /currgstate get
/currpath 1 dict put
} bind def
% - currentpoint x y
% return current point coordinates
/currentpoint {
cpath dup length 0 eq {
pop /currentpoint cvx /nocurrentpoint signalerror
}{
dup length 1 sub get % last-subpath
dup length 1 sub get % last-elem
/data get dup length 2 sub 2 getinterval % last data pair
aload pop itransform
} ifelse
} bind def
/cpath { graphicsdict /currgstate get /currpath get } bind def
/addtopath { % << /data [...] /cmd /... >> <<path>>
%(addtopath)=
%(addtopath start pstack:)= pstack()=
dup length 0 eq { % elem path
1 index /cmd get /move eq { % elem path
%(New Path)=
<< 0 4 3 roll >> % new subpath % <path> <subpath>
0 exch put %
}{
/addtopath cvx /nocurrentpoint signalerror
} ifelse
}{ % elem path
1 index /cmd get /move eq { % elem path
dup dup length 1 sub get % elem path last-subpath
dup length 1 sub get % elem path last-elem-of-last-subpath
dup /cmd get /move eq { % elem path last-elem
%(Merge /move)=
3 1 roll pop % last-elem elem
/data get /data exch put
}{ % elem path last-elem
%(New subpath)=
pop % elem path
dup length 3 2 roll % <path> n <elem>
<< 0 3 2 roll >> % <path> n <<0 <elem>>> % new subpath
%pstack()=
put
} ifelse
}{ % elem path
%(Append elem)=
dup length 1 sub get % elem last-subpath
dup length % elem last-subpath key
3 2 roll
%pstack()=
put
} ifelse
} ifelse
%(addtopath final pstack:)= pstack()=
%(addtopath exit)=
} bind def
% x y moveto -
% set current point to (x,y)
/moveto {
transform
2 array astore
<< /data 3 2 roll /cmd /move >> cpath addtopath
} bind def
% dx dy rmoveto -
% relative moveto
/rmoveto {
currentpoint
3 2 roll add
3 1 roll add exch
moveto
} bind def
% x y lineto -
% append straight line to (x,y)
/lineto {
transform
2 array astore
<< /data 3 2 roll /cmd /line >> cpath addtopath
} bind def
% dx dy rlineto -
% relative lineto
/rlineto {
currentpoint
3 2 roll add
3 1 roll add exch
lineto
} bind def
% x1 y1 x2 y2 x3 y3 curveto -
% append Bezier cubic section
/curveto {
3 { 6 2 roll transform } repeat
6 array astore
<< /data 3 2 roll /cmd /curve >> cpath addtopath
} bind def
You see that the dict has very nice properties for a simulating a growing array. The key for the tail is always at `length 1 sub`. The element dict can hold a size=2 or size=6 array as needed. And everything interacts naturally with save/restore.
The idea I've been toying with for a "new way" is to use stacks. That's the basic auto-growing structure at the C level of the interpreter (lower overhead than a dict because it doesn't exist as a postscript object, so it doesn't take up a slot in the memory table).
With stacks, I think I can arrange the objects to pretty much match the normal postscript syntax.
[0]:0
[1]:0
[2]:moveto
[3]:100
[4]:100
[5]:lineto
[6]:200
[7]:100
[8]:lineto
[9]:closepath
This would be a "subpath" stack or in the original Warnock/Wyatt language, a trajectory as distinct from a complete shape.
To make it nestable, I suppose we have to violate the "no overhead" description above, and wrap the stack in an object. There is precendent for this elsewhere in the interpreter. A savetype object contains a vm address for a stack. Ok, that's still pretty low for overhead. It still doesn't need a memory table slot (aka 'ent').
/**
* @struct Xpost_Object_Save
* @brief The savetype object, for both user and on the save stack.
*/
typedef struct
{
word tag; /**< savetype */
word lev; /**< save-level, index into Save stack */
dword stk; /**< address of Saverec stack */
} Xpost_Object_Save;
So if there's a pathtype object implemented the same way, the "current" path could be a stack of "subpaths" which are pathtype stacks.
[0]:path
[1]:path
[2]:path
And this stack would then be stacked in a top-level "path stack", which is pushed (dup) and popped (... free the stack? ...) in coordination with gsave/grestore and save/restore, since it's no longer contained in the ps-level adt implementation of the gstate stack....
... Hm. Or is that a bad choice? Maybe it can still be in the ps-level gstate dict. I'll have to think on that. That would seem to take care of the (g)*(save/restore) issue.