[cairo] Alternate cairo_path_t representation
Behdad Esfahbod
behdad at behdad.org
Wed Nov 26 11:47:43 PST 2008
Hi,
When writing cairotwisted a couple of years ago I came to conclude that
cairo_path_t currently makes it needlessly hard to use while an alternate
representation will make it much easier. I didn't write it down that time
though as cairo_path_t is here to stay. Hacking on that code again today I
got that same feeling again so I thought I document it at least.
Currently many uses of cairo_path_t have to track both current_point and
last_move_to to be able to track all line and curve segments. The boilerplate
looks like:
int i;
cairo_path_data_t *data, last_move_to, current_point;
for (i=0; i < path->num_data; i += path->data[i].header.length) {
data = &path->data[i];
switch (data->header.type) {
case CAIRO_PATH_MOVE_TO:
last_move_to = data[1];
current_point = data[1];
break;
case CAIRO_PATH_LINE_TO:
process_line_segment (¤t_point, &data[1]);
current_point = data[1];
break;
case CAIRO_PATH_CURVE_TO:
process_curve_segment (¤t_point,
&data[1],
&data[2],
&data[3]);
current_point = data[3];
break;
case CAIRO_PATH_CLOSE_PATH:
process_line_segment (¤t_point, &last_move_to);
break;
default:
g_assert_not_reached ();
}
}
This is unfortunate because the data for both current_point and last_move_to
is always available nearby. The latter is because cairo always does an
implicit move_to after close_path. So, during LINE_TO, CURVE_TO, and
CLOSE_PATH, current_point is always equal to data[-1], and during CLOSE_PATH,
last_move_to is always equal to data[2]. But this is too ugly to advertise
and to use.
Another use case which is unnecessarily complicated is wanting to loop over
all the points only (control points included). One still has to write the
full switch.
An alternate representation that does not suffer from the above issues is to
keep commands and points in separate arrays. That is:
typdef struct _cairo_path_point {
double x, y;
} cairo_path_point_t;
typedef struct _cairo_path_data {
cairo_path_data_type_t type;
cairo_path_point_t *points;
} cairo_path_data_t;
typedef struct cairo_path {
cairo_status_t status;
cairo_path_data_t *data;
int num_data;
cairo_path_point_t *points;
int num_points;
} cairo_path_t;
Where data->points points to current_point for all operations except for
MOVE_TO, followed by the points for the operation at hand. For MOVE_TO, it
points to the new point. The current_point is always there as part of the
points from the previous operation.
The beauty of this is that if one wants to process all the points only, they
can simply loop over path->points directly. Moreover, current_point and
last_move_to are right there in their very natural place when one needs them:
int i;
cairo_path_data_t *data;
for (i=0; i < path->num_data; i++) {
cairo_path_point_t *points;
data = &path->data[i];
points = data->points;
switch (data->type) {
case CAIRO_PATH_MOVE_TO:
break;
case CAIRO_PATH_LINE_TO:
process_line_segment (&points[0], &points[1]);
break;
case CAIRO_PATH_CURVE_TO:
process_curve_segment (&points[0],
&points[1],
&points[2],
&points[3]);
break;
case CAIRO_PATH_CLOSE_PATH:
process_line_segment (&points[0], &points[1]);
break;
default:
g_assert_not_reached ();
}
}
Notice how processing a CLOSE_PATH is exactly the same as processing a LINE_TO.
That's it. Not sure what to do with it. Carl previously said that we can
always add a new function with a new representation if need be. I don't think
this is so different to deserve that. But maybe someone can make use of it
when designing a new API for a new library :).
Cheers,
behdad
More information about the cairo
mailing list