#ifndef __PATH_CONVERTERS_H__
#define __PATH_CONVERTERS_H__
#include "CXX/Objects.hxx"
#include "numpy/arrayobject.h"
#include "agg_path_storage.h"
#include "agg_clip_liang_barsky.h"
#include "MPL_isnan.h"
#include "mplutils.h"
/*
This file contains a number of vertex converters that modify
paths. They all work as iterators, where the output is generated
on-the-fly, and don't require a copy of the full data.
Each class represents a discrete step in a "path-cleansing" pipeline.
They are currently applied in the following order in the Agg backend:
1. Affine transformation (implemented in Agg, not here)
2. PathNanRemover: skips over segments containing non-finite numbers
by inserting MOVETO commands
3. PathClipper: Clips line segments to a given rectangle. This is
helpful for data reduction, and also to avoid a limitation in
Agg where coordinates can not be larger than 24-bit signed
integers.
4. PathQuantizer: Rounds the path to the nearest center-pixels.
This makes rectilinear curves look much better.
5. PathSimplifier: Removes line segments from highly dense paths
that would not have an impact on their appearance. Speeds up
rendering and reduces file sizes.
6. curve-to-line-segment conversion (implemented in Agg, not here)
7. stroking (implemented in Agg, not here)
*/
/************************************************************
This is a base class for vertex converters that need to queue their
output. It is designed to be as fast as possible vs. the STL's queue
which is more flexible.
*/
template<int QueueSize>
class EmbeddedQueue
{
protected:
EmbeddedQueue() :
m_queue_read(0), m_queue_write(0)
{
// empty
}
struct item
{
item() {}
inline void set(const unsigned cmd_, const double& x_, const double& y_)
{
cmd = cmd_;
x = x_;
y = y_;
}
unsigned cmd;
double x;
double y;
};
int m_queue_read;
int m_queue_write;
item m_queue[QueueSize];
inline void queue_push(const unsigned cmd, const double& x, const double& y)
{
m_queue[m_queue_write++].set(cmd, x, y);
}
inline bool queue_nonempty()
{
return m_queue_read < m_queue_write;
}
inline bool queue_flush(unsigned *cmd, double *x, double *y)
{
if (queue_nonempty())
{
const item& front = m_queue[m_queue_read++];
*cmd = front.cmd;
*x = front.x;
*y = front.y;
return true;
}
m_queue_read = 0;
m_queue_write = 0;
return false;
}
inline void queue_clear()
{
m_queue_read = 0;
m_queue_write = 0;
}
};
/*
PathNanRemover is a vertex converter that removes non-finite values
from the vertices list, and inserts MOVETO commands as necessary to
skip over them. If a curve segment contains at least one non-finite
value, the entire curve segment will be skipped.
*/
template<class VertexSource>
class PathNanRemover : protected EmbeddedQueue<4> {
VertexSource* m_source;
bool m_remove_nans;
bool m_has_curves;
static const unsigned char num_extra_points_map[16];
public:
/* has_curves should be true if the path contains bezier curve
segments, as this requires a slower algorithm to remove the
NaNs. When in doubt, set to true.
*/
PathNanRemover(VertexSource& source, bool remove_nans, bool has_curves) :
m_source(&source), m_remove_nans(remove_nans), m_has_curves(has_curves)
{
// empty
}
inline void rewind(unsigned path_id)
{
queue_clear();
m_source->rewind(path_id);
}
inline unsigned vertex(double* x, double *y) {
unsigned code;
if (!m_remove_nans) {
return m_source->vertex(x, y);
}
if (m_has_curves) {
/* This is the slow method for when there might be curves. */
if (queue_flush(&code, x, y))
{
return code;
}
bool needs_move_to = false;
while (true) {
/* The approach here is to push each full curve
segment into the queue. If any non-finite values
are found along the way, the queue is emptied, and
the next curve segment is handled. */
code = m_source->vertex(x, y);
if (code == agg::path_cmd_stop ||
code == (agg::path_cmd_end_poly | agg::path_flags_close))
{
return code;
}
if (needs_move_to)
{
queue_push(agg::path_cmd_move_to, *x, *y);
}
size_t num_extra_points = num_extra_points_map[code & 0xF];
bool has_nan = (MPL_notisfinite64(*x) || MPL_notisfinite64(*y));
queue_push(code, *x, *y);
/* Note: this test can not be short-circuited, since we need to
advance through the entire curve no matter what */
for (size_t i = 0; i < num_extra_points; ++i)
{
m_source->vertex(x, y);
has_nan |= (MPL_notisfinite64(*x) || MPL_notisfinite64(*y));
queue_push(code, *x, *y);
}
if (!has_nan)
{
break;
}
queue_clear();
/* If the last point is finite, we use that for the
moveto, otherwise, we'll use the first vertex of
the next curve. */
if (!(MPL_notisfinite64(*x) || MPL_notisfinite64(*y)))
{
queue_push(agg::path_cmd_move_to, *x, *y);
needs_move_to = false;
}
else
{
needs_move_to = true;
}
}
if (queue_flush(&code, x, y))
{
return code;
}
else
{
return agg::path_cmd_stop;
}
}
else // !m_has_curves
{
/* This is the fast path for when we know we have no curves */
code = m_source->vertex(x, y);
if (code == agg::path_cmd_stop ||
code == (agg::path_cmd_end_poly | agg::path_flags_close))
{
return code;
}
if (MPL_notisfinite64(*x) || MPL_notisfinite64(*y))
{
do
{
code = m_source->vertex(x, y);
if (code == agg::path_cmd_stop ||
code == (agg::path_cmd_end_poly | agg::path_flags_close))
{
return code;
}
} while (MPL_notisfinite64(*x) || MPL_notisfinite64(*y));
return agg::path_cmd_move_to;
}
}
return code;
}
};
/* Defines when path segment types have more than one vertex */
template<class VertexSource>
const unsigned char PathNanRemover<VertexSource>::num_extra_points_map[] =
{0, 0, 0, 1,
2, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0};
/************************************************************
PathClipper uses the Liang-Barsky line clipping algorithm (as
implemented in Agg) to clip the path to a given rectangle. Lines
will never extend outside of the rectangle. Curve segments are not
clipped, but are always included in their entirety.
*/
template<class VertexSource>
class PathClipper
{
VertexSource* m_source;
bool m_do_clipping;
agg::rect_base<double> m_cliprect;
double m_lastX;
double m_lastY;
bool m_moveto;
double m_nextX;
double m_nextY;
bool m_has_next;
public:
PathClipper(VertexSource& source, bool do_clipping,
double width, double height) :
m_source(&source), m_do_clipping(do_clipping),
m_cliprect(0.0, 0.0, width, height), m_moveto(true),
m_has_next(false)
{
// empty
}
PathClipper(VertexSource& source, bool do_clipping,
const agg::rect_base<double>& rect) :
m_source(&source), m_do_clipping(do_clipping),
m_cliprect(rect), m_moveto(true), m_has_next(false)
{
// empty
}
inline void rewind(unsigned path_id)
{
m_has_next = false;
m_moveto = true;
m_source->rewind(path_id);
}
unsigned vertex(double* x, double* y)
{
unsigned code;
if (m_do_clipping) {
/* This is the slow path where we actually do clipping */
if (m_has_next) {
m_has_next = false;
*x = m_nextX;
*y = m_nextY;
return agg::path_cmd_line_to;
}
while ((code = m_source->vertex(x, y)) != agg::path_cmd_stop) {
if (m_moveto)
{
m_moveto = false;
code = agg::path_cmd_move_to;
break;
}
else if (code == agg::path_cmd_line_to)
{
double x0, y0, x1, y1;
x0 = m_lastX;
y0 = m_lastY;
x1 = *x;
y1 = *y;
m_lastX = *x;
m_lastY = *y;
unsigned moved = agg::clip_line_segment(&x0, &y0, &x1, &y1, m_cliprect);
// moved >= 4 - Fully clipped
// moved & 1 != 0 - First point has been moved
// moved & 2 != 0 - Second point has been moved
if (moved < 4)
{
if (moved & 1)
{
*x = x0;
*y = y0;
m_nextX = x1;
m_nextY = y1;
m_has_next = true;
return agg::path_cmd_move_to;
}
*x = x1;
*y = y1;
return code;
}
}
else
{
break;
}
}
m_lastX = *x;
m_lastY = *y;
return code;
}
else
{
// If not doing any clipping, just pass along the vertices
// verbatim
return m_source->vertex(x, y);
}
}
};
/************************************************************
PathQuantizer rounds vertices to their nearest center-pixels. This
makes rectilinear paths (rectangles, horizontal and vertical lines
etc.) look much cleaner.
*/
enum e_quantize_mode
{
QUANTIZE_AUTO,
QUANTIZE_FALSE,
QUANTIZE_TRUE
};
template<class VertexSource>
class PathQuantizer
{
private:
VertexSource* m_source;
bool m_quantize;
static bool should_quantize(VertexSource& path,
e_quantize_mode quantize_mode,
unsigned total_vertices) {
// If this contains only straight horizontal or vertical lines, it should be
// quantized to the nearest pixels
double x0, y0, x1, y1;
unsigned code;
switch (quantize_mode)
{
case QUANTIZE_AUTO:
if (total_vertices > 1024)
{
return false;
}
code = path.vertex(&x0, &y0);
if (code == agg::path_cmd_stop)
{
return false;
}
while ((code = path.vertex(&x1, &y1)) != agg::path_cmd_stop)
{
switch (code)
{
case agg::path_cmd_curve3:
case agg::path_cmd_curve4:
return false;
case agg::path_cmd_line_to:
if (!(fabs(x0 - x1) < 1e-4 || fabs(y0 - y1) < 1e-4))
{
return false;
}
}
x0 = x1;
y0 = y1;
}
return true;
case QUANTIZE_FALSE:
return false;
case QUANTIZE_TRUE:
return true;
}
return false;
}
public:
/*
quantize_mode should be one of:
- QUANTIZE_AUTO: Examine the path to determine if it should be quantized
- QUANTIZE_TRUE: Force quantization
- QUANTIZE_FALSE: No quantization
*/
PathQuantizer(VertexSource& source, e_quantize_mode quantize_mode,
unsigned total_vertices=15) :
m_source(&source)
{
m_quantize = should_quantize(source, quantize_mode, total_vertices);
source.rewind(0);
}
inline void rewind(unsigned path_id)
{
m_source->rewind(path_id);
}
inline unsigned vertex(double* x, double* y)
{
unsigned code;
code = m_source->vertex(x, y);
if (m_quantize && agg::is_vertex(code))
{
*x = mpl_round(*x) + 0.5;
*y = mpl_round(*y) + 0.5;
}
return code;
}
inline bool is_quantizing()
{
return m_quantize;
}
};
/************************************************************
PathSimplifier reduces the number of vertices in a dense path without
changing its appearance.
*/
template<class VertexSource>
class PathSimplifier : protected EmbeddedQueue<9>
{
public:
/* Set simplify to true to perform simplification */
PathSimplifier(VertexSource& source, bool do_simplify, double simplify_threshold) :
m_source(&source), m_simplify(do_simplify),
m_simplify_threshold(simplify_threshold),
m_moveto(true), m_after_moveto(false),
m_lastx(0.0), m_lasty(0.0), m_clipped(false),
m_origdx(0.0), m_origdy(0.0),
m_origdNorm2(0.0), m_dnorm2Max(0.0), m_dnorm2Min(0.0),
m_lastMax(false), m_nextX(0.0), m_nextY(0.0),
m_lastWrittenX(0.0), m_lastWrittenY(0.0)
{
// empty
}
inline void rewind(unsigned path_id)
{
queue_clear();
m_moveto = true;
m_source->rewind(path_id);
}
unsigned vertex(double* x, double* y)
{
unsigned cmd;
/* The simplification algorithm doesn't support curves or compound paths
so we just don't do it at all in that case... */
if (!m_simplify)
{
return m_source->vertex(x, y);
}
/* idea: we can skip drawing many lines: lines < 1 pixel in
length, and we can combine sequential parallel lines into a
single line instead of redrawing lines over the same
points. The loop below works a bit like a state machine,
where what it does depends on what it did in the last
looping. To test whether sequential lines are close to
parallel, I calculate the distance moved perpendicular to
the last line. Once it gets too big, the lines cannot be
combined. */
/* This code was originally written by Allan Haldane and I
have modified to work in-place -- meaning not creating an
entirely new path list each time. In order to do that
without too much additional code complexity, it keeps a
small queue around so that multiple points can be emitted
in a single call, and those points will be popped from the
queue in subsequent calls. The following block will empty
the queue before proceeding to the main loop below.
-- Michael Droettboom */
if (queue_flush(&cmd, x, y)) {
return cmd;
}
/* The main simplification loop. The point is to consume only
as many points as necessary until something has been added
to the outbound queue, not to run through the entire path
in one go. This eliminates the need to allocate and fill
an entire additional path array on each draw. */
while ((cmd = m_source->vertex(x, y)) != agg::path_cmd_stop)
{
/* if we are starting a new path segment, move to the first point
+ init */
if (m_moveto || cmd == agg::path_cmd_move_to)
{
/* m_moveto check is not generally needed because
m_source generates an initial moveto; but it is
retained for safety in case circumstances arise
where this is not true. */
if (m_origdNorm2 != 0.0 && !m_after_moveto)
{
/* m_origdNorm2 is nonzero only if we have a
vector; the m_after_moveto check ensures we
push this vector to the queue only once. */
_push(x,y);
}
m_after_moveto = true;
m_lastx = *x;
m_lasty = *y;
m_moveto = false;
m_origdNorm2 = 0.0;
m_clipped = true;
if (queue_nonempty())
{
/* If we did a push, empty the queue now. */
break;
}
continue;
}
m_after_moveto = false;
/* Don't render line segments less than one pixel long */
if (fabs(*x - m_lastx) < 1.0 && fabs(*y - m_lasty) < 1.0)
{
continue;
}
/* if we have no orig vector, set it to this vector and
continue. this orig vector is the reference vector we
will build up the line to */
if (m_origdNorm2 == 0.0)
{
if (m_clipped)
{
queue_push(agg::path_cmd_move_to, m_lastx, m_lasty);
m_clipped = false;
}
m_origdx = *x - m_lastx;
m_origdy = *y - m_lasty;
m_origdNorm2 = m_origdx*m_origdx + m_origdy*m_origdy;
//set all the variables to reflect this new orig vector
m_dnorm2Max = m_origdNorm2;
m_dnorm2Min = 0.0;
m_lastMax = true;
m_nextX = m_lastWrittenX = m_lastx = *x;
m_nextY = m_lastWrittenY = m_lasty = *y;
continue;
}
/* If got to here, then we have an orig vector and we just got
a vector in the sequence. */
/* Check that the perpendicular distance we have moved
from the last written point compared to the line we are
building is not too much. If o is the orig vector (we
are building on), and v is the vector from the last
written point to the current point, then the
perpendicular vector is p = v - (o.v)o, and we
normalize o (by dividing the second term by o.o). */
/* get the v vector */
double totdx = *x - m_lastWrittenX;
double totdy = *y - m_lastWrittenY;
double totdot = m_origdx*totdx + m_origdy*totdy;
/* get the para vector ( = (o.v)o/(o.o)) */
double paradx = totdot*m_origdx/m_origdNorm2;
double parady = totdot*m_origdy/m_origdNorm2;
/* get the perp vector ( = v - para) */
double perpdx = totdx - paradx;
double perpdy = totdy - parady;
double perpdNorm2 = perpdx*perpdx + perpdy*perpdy;
/* If the perp vector is less than some number of (squared)
pixels in size, then merge the current vector */
if (perpdNorm2 < m_simplify_threshold)
{
/* check if the current vector is parallel or
anti-parallel to the orig vector. If it is
parallel, test if it is the longest of the vectors
we are merging in that direction. If anti-p, test
if it is the longest in the opposite direction (the
min of our final line) */
double paradNorm2 = paradx*paradx + parady*parady;
m_lastMax = false;
if (totdot >= 0.0)
{
if (paradNorm2 > m_dnorm2Max)
{
m_lastMax = true;
m_dnorm2Max = paradNorm2;
m_nextX = *x;
m_nextY = *y;
}
}
else
{
if (paradNorm2 > m_dnorm2Min)
{
m_dnorm2Min = paradNorm2;
m_nextX = *x;
m_nextY = *y;
}
}
m_lastx = *x;
m_lasty = *y;
continue;
}
/* If we get here, then this vector was not similar enough to the
line we are building, so we need to draw that line and start the
next one. */
/* If the line needs to extend in the opposite direction from the
direction we are drawing in, move back to we start drawing from
back there. */
_push(x, y);
break;
}
/* Fill the queue with the remaining vertices if we've finished the
path in the above loop. */
if (cmd == agg::path_cmd_stop)
{
if (m_origdNorm2 != 0.0)
{
queue_push(agg::path_cmd_line_to, m_nextX, m_nextY);
}
queue_push(agg::path_cmd_line_to, m_lastx, m_lasty);
queue_push(agg::path_cmd_stop, 0.0, 0.0);
}
/* Return the first item in the queue, if any, otherwise
indicate that we're done. */
if (queue_flush(&cmd, x, y)) {
return cmd;
}
else
{
return agg::path_cmd_stop;
}
}
private:
VertexSource* m_source;
bool m_simplify;
double m_simplify_threshold;
bool m_moveto;
bool m_after_moveto;
double m_lastx, m_lasty;
bool m_clipped;
double m_origdx;
double m_origdy;
double m_origdNorm2;
double m_dnorm2Max;
double m_dnorm2Min;
bool m_lastMax;
double m_nextX;
double m_nextY;
double m_lastWrittenX;
double m_lastWrittenY;
inline void _push(double* x, double* y)
{
queue_push(agg::path_cmd_line_to, m_nextX, m_nextY);
/* If we clipped some segments between this line and the next line
we are starting, we also need to move to the last point. */
if (m_clipped)
{
queue_push(agg::path_cmd_move_to, m_lastx, m_lasty);
}
else if (!m_lastMax)
{
/* If the last line was not the longest line, then move
back to the end point of the last line in the
sequence. Only do this if not clipped, since in that
case lastx,lasty is not part of the line just drawn. */
/* Would be move_to if not for the artifacts */
queue_push(agg::path_cmd_line_to, m_lastx, m_lasty);
}
/* Now reset all the variables to get ready for the next line */
m_origdx = *x - m_lastx;
m_origdy = *y - m_lasty;
m_origdNorm2 = m_origdx*m_origdx + m_origdy*m_origdy;
m_dnorm2Max = m_origdNorm2;
m_dnorm2Min = 0.0;
m_lastMax = true;
m_lastWrittenX = m_queue[m_queue_write-1].x;
m_lastWrittenY = m_queue[m_queue_write-1].y;
m_lastx = m_nextX = *x;
m_lasty = m_nextY = *y;
m_clipped = false;
}
};
#endif // __PATH_CONVERTERS_H__