Blob Blame Raw

#include "private.h"
#include "world.h"
#include "drawing.h"


static double colorBack[4] = {1, 1, 1, 1};
static double *path;
static size_t pathSize;
static size_t pathAllocated;

static HeliDrawingState statesStack[1024] = {{
	0,                    // flags
	{0.5, 0.5, 0.5, 1},   // fillColor
	{0, 0, 0, 1},         // strokeColor
	1,                    // lineWidth
	HALIGN_LEFT,          // horAlign
	VALIGN_TOP,           // vertAlign
	24                    // fontSize
}};
static int statesStackIndex = 0;



typedef struct _HeliStrokePoint {
	double x, y, dx, dy;
} HeliStrokePoint;

typedef struct _HeliStroke {
	HeliStrokePoint *points;
	int count;
	int allocatedCount;
} HeliStroke;

static void endPath(int close, int stroke, int fill, int fillSimple);

static void closePathSimple()
	{ endPath(TRUE, TRUE, FALSE, TRUE); }


void heliDrawingApplyTexture(double *color, HeliTextureState *state) {
	glColor4dv(color);
	unsigned int texid = state->animation ? animationGetGLTexId(state->animation) : 0;
	if (texid) {
		unsigned int mode = state->fixed ? GL_EYE_LINEAR : GL_OBJECT_LINEAR;
		unsigned int param = state->fixed ? GL_EYE_PLANE : GL_OBJECT_PLANE;
		
		double kx = fabs(state->width) > HELI_PRECISION ? 1/state->width
		          : state->width < 0 ? -HELI_PRECISION : HELI_PRECISION;
		double ky = fabs(state->height) > HELI_PRECISION ? 1/state->height
		          : state->height < 0 ? -HELI_PRECISION : HELI_PRECISION;
		double x[4] = { kx, 0, 0, -state->x*kx };
		double y[4] = { 0, ky, 0, -state->y*ky };
		
		glEnable(GL_TEXTURE_2D);
		glBindTexture(GL_TEXTURE_2D, texid);
		glEnable(GL_TEXTURE_GEN_S);
		glEnable(GL_TEXTURE_GEN_T);
		glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, mode);
		glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, mode);
		glTexGendv(GL_S, param, x);
		glTexGendv(GL_T, param, y);
	}
}

void heliDrawingResetTexture() {
	glBindTexture(GL_TEXTURE_2D, 0);
	glDisable(GL_TEXTURE_GEN_S);
	glDisable(GL_TEXTURE_GEN_T);
	glDisable(GL_TEXTURE_2D);
	glColor4d(1, 1, 1, 1);
}


void background(unsigned int colorCode)
	{ heliColorToDouble(colorCode, colorBack); }
void fill(unsigned int colorCode)
	{ heliColorToDouble(colorCode, heliDrawingGetState()->fillColor); }
void noFill()
	{ fill(0); }
void fillTexture(Animation animation, double x, double y, double width, double height, int fixed) {
	HeliTextureState *s = &heliDrawingGetState()->fillTexture; 
	s->animation = animation;
	s->x = x;
	s->y = y;
	s->width = width;
	s->height = height;
	s->fixed = fixed != 0;
}

void stroke(unsigned int colorCode)
	{ heliColorToDouble(colorCode, heliDrawingGetState()->strokeColor); }
void noStroke()
	{ stroke(0); }
void strokeTexture(Animation animation, double x, double y, double width, double height, int fixed) {
	HeliTextureState *s = &heliDrawingGetState()->strokeTexture; 
	s->animation = animation;
	s->x = x;
	s->y = y;
	s->width = width;
	s->height = height;
	s->fixed = fixed != 0;
}

void strokeWidth(double width)
	{ heliDrawingGetState()->strokeWidth = width; }

const char* rgba(double r, double g, double b, double a) {
	static char buf[1024];
	snprintf(buf, sizeof(buf) - 1, "%f %f %f %f", r, g, b, a);
	return buf;
}

const char* rgb(double r, double g, double b)
	{ return rgba(r, g, b, 1); }

void translate(double x, double y)
	{ glTranslated(x, y, 0); }
void rotate(double angle)
	{ glRotated(angle, 0, 0, 1); }
void scale(double x, double y)
	{ glScaled(x, y, 1); }
void zoom(double z)
	{ scale(z, z); }

void cliprect(double x, double y, double width, double height) {
	double eq[4][4] = {
		{ 1, 0, 0, -x        },
		{-1, 0, 0,  x+width  },
		{ 0, 1, 0, -y        },
		{ 0,-1, 0,  y+height } };
	for(int i = 0; i < 4; ++i) {
		glEnable(GL_CLIP_PLANE0 + i);
		glClipPlane(GL_CLIP_PLANE0 + i, eq[i]);
	}
}

void noClip()
	{ for(int i = 0; i < 4; ++i) glDisable(GL_CLIP_PLANE0 + i); }

void rect(double x, double y, double width, double height) {
	resetPath();
	heliFillRectSimple(x, y, x+width, y+height, heliGLGetAAResolution());
	moveTo(x, y);
	lineTo(x + width, y);
	lineTo(x + width, y + height);
	lineTo(x, y + height);
	endPath(TRUE, TRUE, FALSE, FALSE);
}

void line(double x1, double y1, double x2, double y2) {
	resetPath();
	moveTo(x1, y1);
	lineTo(x2, y2);
	strokePath();
}

void ellipse(double x, double y, double width, double height) {
	resetPath();
	if (fabs(fabs(width) - fabs(height)) <= HELI_PRECISION) {
		heliFillCircleSimple(x + 0.5*width, y + 0.5*height, 0.5*width, heliGLGetAAResolution());
		arcPath(x, y, width, height, 0, 360);
		endPath(TRUE, TRUE, FALSE, FALSE);
	} else {
		arcPath(x, y, width, height, 0, 360);
		closePathSimple();
	}
}

void point(double x, double y) {
	saveStateEx(STATE_FILL | STATE_STROKE);
	HeliDrawingState *s = heliDrawingGetState();
	memcpy(&s->fillColor, &s->strokeColor, sizeof(s->fillColor));
	memcpy(&s->fillTexture, &s->strokeTexture, sizeof(s->fillTexture));
	noStroke();
	if (s->strokeWidth > HELI_PRECISION)
		ellipse(x - s->strokeWidth*0.5, y - s->strokeWidth*0.5, s->strokeWidth, s->strokeWidth);
	restoreState();
}

void arcPath(double x, double y, double w, double h, double start, double stop) {
	w *= 0.5; x += w;
	h *= 0.5; y += h;
	if (fabs(w) < HELI_PRECISION || fabs(h) < HELI_PRECISION) return;
	double step = fabs(w) > fabs(h) ? 1/fabs(w) : 1/fabs(h);
	if (step > PI/180) step = PI/180;
	start *= PI/180;
	stop *= PI/180;
	double a = start;
	if (start < stop) {
		while(1) {
			if (a > stop) a = stop;
			lineTo(x + cos(a)*w, y + sin(a)*h);
			if (a == stop) break;
			a += step;
		}
	} else {
		while(1) {
			if (a < stop) a = stop;
			lineTo(x + cos(a)*w, y + sin(a)*h);
			if (a == stop) break;
			a -= step;
		}
	}
}

void arc(double x, double y, double w, double h, double start, double stop) {
	resetPath();
	arcPath(x, y, w, h, start, stop);
	strokePath();
}

void regularPolygon(double x, double y, int sides, double size) {
	resetPath();
	size *= 0.5;
	moveTo(x + size, y);
	for(int i = 1; i < sides; ++i) {
		double a = i*2*PI/sides;
		lineTo(x + size*cos(a), y + size*sin(a));
	}
	closePathSimple();
}


void resetPath()
	{ pathSize = 0; }


void heliFillCircleSimple(double x, double y, double r, double aaBorder) {
	r = fabs(r);
	if (r < HELI_PRECISION) return;
	
	HeliDrawingState *s = heliDrawingGetState();
	if (s->fillColor[3] <= HELI_PRECISION) return;
	
	heliDrawingApplyTexture(s->fillColor, &s->fillTexture);
	
	double step = 1/r;
	if (step > PI/180) step = PI/180;
	if (aaBorder < HELI_PRECISION) {
		glEnable(GL_MULTISAMPLE);
		glBegin(GL_TRIANGLE_FAN);
		for(double a = 0; a < 2*PI; a += step) glVertex2d(x + cos(a)*r, y + sin(a)*r);
		glEnd();
		glDisable(GL_MULTISAMPLE);
	} else {
		double color[4] = { s->fillColor[0], s->fillColor[1], s->fillColor[2], s->fillColor[3] };
		double sideColor[4] = { color[0], color[1], color[2], 0 };
		double r1 = r + 0.5*aaBorder;
		double r0 = r - 0.5*aaBorder;
		if (r0 > HELI_PRECISION) {
			glBegin(GL_TRIANGLE_FAN);
			for(double a = 0; a < 2*PI; a += step) glVertex2d(x + cos(a)*r0, y + sin(a)*r0);
			glEnd();
		} else {
			color[3] *= r/aaBorder;
			r0 = 0;
		}
		glBegin(GL_TRIANGLE_STRIP);
		for(double a = step; a < 2*PI; a += step) {
			double s = sin(a), c = cos(a);
			glColor4dv(color);
			glVertex2d(x + c*r0, y + s*r0);
			glColor4dv(sideColor);
			glVertex2d(x + c*r1, y + s*r1);
		}
		glEnd();
	}
}


void heliFillRectSimple(double x0, double y0, double x1, double y1, double aaBorder) {
	if (fabs(x1 - x0) < HELI_PRECISION || fabs(y1 - y0) < HELI_PRECISION) return;
	
	HeliDrawingState *s = heliDrawingGetState();
	if (s->fillColor[3] <= HELI_PRECISION) return;
	
	heliDrawingApplyTexture(s->fillColor, &s->fillTexture);
	if (aaBorder <= HELI_PRECISION) {
		glEnable(GL_MULTISAMPLE);
		glBegin(GL_QUADS);
		glVertex2d(x0, y0);
		glVertex2d(x1, y0);
		glVertex2d(x1, y1);
		glVertex2d(x0, y1);
		glEnd();
		glDisable(GL_MULTISAMPLE);
	} else {
		double color[4] = { s->fillColor[0], s->fillColor[1], s->fillColor[2], s->fillColor[3] };
		double sideColor[4] = { color[0], color[1], color[2], 0 };
		
		if (x1 < x0) { double x = x0; x0 = x1; x1 = x; }
		if (y1 < y0) { double y = y0; y0 = y1; y1 = y; }
		double x  = (x1 + x0)*0.5;
		double y  = (y1 + y0)*0.5;
		double hw = (x1 - x0)*0.5;
		double hh = (y1 - y0)*0.5;
		
		double w0 = hw - 0.5*aaBorder;
		double w1 = hw + 0.5*aaBorder;
		if (w0 < HELI_PRECISION) w0 = 0;
		double h0 = hh - 0.5*aaBorder;
		double h1 = hh + 0.5*aaBorder;
		if (h0 < HELI_PRECISION) h0 = 0;
		
		const double k = (w1 < aaBorder ? w1/aaBorder : 1)
		               * (h1 < aaBorder ? h1/aaBorder : 1);
		color[3] *= k;
		
		const int vcnt = 14;
		
		const double vertices[][2] = {
			{ -w1, -h1 }, { -w0, -h0 },
			{  w1, -h1 }, {  w0, -h0 },
			{  w1,  h1 }, {  w0,  h0 },
			{ -w1,  h1 }, { -w0,  h0 },
			{ -w1, -h1 }, { -w0, -h0 },
			{ -w0, -h0 },
			{  w0, -h0 },
			{ -w0,  h0 },
			{  w0,  h0 } };
		
		const double *colors[] = {
			sideColor, color,
			sideColor, color,
			sideColor, color,
			sideColor, color,
			sideColor, color,
			color,
			color,
			color,
			color };
		
		glPushMatrix();
		glTranslated(x, y, 0);
		glBegin(GL_TRIANGLE_STRIP);
		for(int i = 0; i < vcnt; ++i) {
			glColor4dv(colors[i]);
			glVertex2dv(vertices[i]);
		}
		glEnd();
		glPopMatrix();
	}
	heliDrawingResetTexture();
}


static void drawFillSimple() {
	HeliDrawingState *s = heliDrawingGetState();
	if (s->fillColor[3] <= HELI_PRECISION) return;
	if (pathSize < 6) return;
	
	heliDrawingApplyTexture(s->fillColor, &s->fillTexture);
	glEnable(GL_MULTISAMPLE);
	glBegin(GL_TRIANGLE_FAN);
	for(int i = 0; i < pathSize; i += 2)
		glVertex2dv(&path[i]);
	glEnd();
	glDisable(GL_MULTISAMPLE);
	heliDrawingResetTexture();
}

static void drawFill() {
	HeliDrawingState *s = heliDrawingGetState();
	if (s->fillColor[3] <= HELI_PRECISION) return;
	if (pathSize < 6) return;
	
	double l = path[0], r = l;
	double t = path[1], b = t;
	for(int i = 2; i < pathSize; i += 2) {
		if (l > path[i]) l = path[i];
		if (r < path[i]) r = path[i];
		if (t > path[i + 1]) t = path[i + 1];
		if (b < path[i + 1]) b = path[i + 1];
	}
	if ( r - l <= HELI_PRECISION
	  || b - t <= HELI_PRECISION) return;
	l -= 1; r += 1;
	t -= 1; b += 1;
	
	glEnable(GL_MULTISAMPLE);
	glEnable(GL_STENCIL_TEST);
	glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);

	glStencilOp(GL_ZERO, GL_ZERO, GL_ZERO);
	glBegin(GL_QUADS);
	glVertex2d(l, t);
	glVertex2d(r, t);
	glVertex2d(r, b);
	glVertex2d(l, b);
	glEnd();
	
	if (heliGlStencilOpSeparatePtr) {
		heliGlStencilOpSeparatePtr(GL_FRONT, GL_INCR_WRAP, GL_INCR_WRAP, GL_INCR_WRAP);
		heliGlStencilOpSeparatePtr(GL_BACK, GL_DECR_WRAP, GL_DECR_WRAP, GL_DECR_WRAP);
		glBegin(GL_TRIANGLE_FAN);
		for(int i = 0; i < pathSize; i += 2)
			glVertex2dv(&path[i]);
		glEnd();
	} else {
		glEnable(GL_CULL_FACE);
		
		glStencilOp(GL_DECR_WRAP, GL_DECR_WRAP, GL_DECR_WRAP);
		glCullFace(GL_FRONT);
		glBegin(GL_TRIANGLE_FAN);
		for(int i = 0; i < pathSize; i += 2)
			glVertex2dv(&path[i]);
		glEnd();
		
		glStencilOp(GL_INCR_WRAP, GL_INCR_WRAP, GL_INCR_WRAP);
		glCullFace(GL_BACK);
		glBegin(GL_TRIANGLE_FAN);
		for(int i = 0; i < pathSize; i += 2)
			glVertex2dv(&path[i]);
		glEnd();
		
		glDisable(GL_CULL_FACE);
	}
	
	glStencilFunc(GL_NOTEQUAL, 0, -1u);
	glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
	glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);

	heliDrawingApplyTexture(s->fillColor, &s->fillTexture);
	glBegin(GL_QUADS);
	glVertex2d(l, t);
	glVertex2d(r, t);
	glVertex2d(r, b);
	glVertex2d(l, b);
	glEnd();
	heliDrawingResetTexture();
	
	glStencilFunc(GL_ALWAYS, 0, -1u);
	glDisable(GL_STENCIL_TEST);
	glDisable(GL_MULTISAMPLE);
}

static void strokeAddPoint(HeliStroke *stroke, double x, double y, double dx, double dy) {
	if (stroke->allocatedCount < stroke->count + 1) {
		stroke->allocatedCount += stroke->allocatedCount/4 + 32;
		stroke->points = realloc(stroke->points, stroke->allocatedCount*4*sizeof(*stroke->points));
		memset(stroke->points + stroke->count, 0, (stroke->allocatedCount - stroke->count)*sizeof(*stroke->points));
	}
	HeliStrokePoint *p = &stroke->points[ stroke->count++ ];
	p->x = x;
	p->y = y;
	p->dx = dx;
	p->dy = dy;
}

static void strokeAddCornerSub(HeliStroke *stroke, double dx, double dy, double precisionSqr, int level) {
	assert(stroke->count > 0);
	HeliStrokePoint *prev = stroke->points + stroke->count - 1;
	if (level > 0) {
		double distx = dx - prev->dx;
		double disty = dy - prev->dy;
		if (distx*distx + disty*disty > precisionSqr) {
			double mx = prev->dx + dx;
			double my = prev->dy + dy;
			double lsqr = mx*mx + my*my;
			if (lsqr > HELI_PRECISION_SQR) {
				double kl = 1/sqrt(lsqr);
				mx *= kl;
				my *= kl;
				strokeAddCornerSub(stroke, mx, my, precisionSqr, level-1);
				strokeAddCornerSub(stroke, dx, dy, precisionSqr, level-1);
				return;
			}
		}
	}
	strokeAddPoint(stroke, prev->x, prev->y, dx, dy);
}

static void strokeAddCorner(
	HeliStroke *stroke,
	double xp, double yp,
	double x0, double y0,
	double x1, double y1,
	double precisionSqr )
{
	double dx0 = x0 - xp;
	double dy0 = y0 - yp;
	double dx1 = x1 - x0;
	double dy1 = y1 - y0;
	
	double l0 = sqrt(dx0*dx0 + dy0*dy0);
	double l1 = sqrt(dx1*dx1 + dy1*dy1);
	assert(l0 > HELI_PRECISION);
	assert(l1 > HELI_PRECISION);
	
	double kl0 = 1/l0, kl1 = 1/l1;
	double nx0 = dx0*kl0, ny0 = dy0*kl0;
	double nx1 = dx1*kl1, ny1 = dy1*kl1;
	
	double dot = nx0*ny1 - ny0*nx1;
	if (dot > HELI_PRECISION) {
		// round
		strokeAddPoint(stroke, x0, y0, ny0, -nx0);
		strokeAddCornerSub(stroke, ny1, -nx1, precisionSqr, 8);
	} else
	if (dot < HELI_PRECISION) {
		// corner
		double dx = nx1 - nx0;
		double dy = ny1 - ny0;
		double kl = 1/sqrt(dx*dx + dy*dy);
		double nx = dx*kl, ny = dy*kl;
		double c = nx*nx1 + ny*ny1;
		double cl = 1/sqrt(1 - c*c);
		if (cl > 3) cl = 3;
		strokeAddPoint(stroke, x0, y0, nx*cl, ny*cl);
	} else {
		// flat
		strokeAddPoint(stroke, x0, y0, ny0, -nx0);
	}
}

static void strokeDraw(HeliStroke *stroke, double w, double *color) {
	const double aaBorder = heliGLGetAAResolution();
	if (aaBorder > HELI_PRECISION) {
		double c0[4] = { color[0], color[1], color[2], 0 };
		double c1[4] = { color[0], color[1], color[2], color[3] };
		double w0 = w + 0.5*aaBorder;
		double w1 = w - 0.5*aaBorder;
		if (w1 < HELI_PRECISION) {
			c1[3] *= w0/aaBorder;
			w1 = 0;
		}
		
		glBegin(GL_TRIANGLE_STRIP);
		for(int i = 0; i < stroke->count; ++i) {
			HeliStrokePoint *p = stroke->points + i;
			glColor4dv(c0);
			glVertex2d(p->x + p->dx*w0, p->y + p->dy*w0);
			glColor4dv(c1);
			glVertex2d(p->x + p->dx*w1, p->y + p->dy*w1);
		}
		glEnd();
		
		if (w1 > 0) {
			glColor4dv(c1);
			glBegin(GL_TRIANGLE_STRIP);
			for(int i = 0; i < stroke->count; ++i) {
				HeliStrokePoint *p = stroke->points + i;
				glVertex2d(p->x, p->y);
				glVertex2d(p->x + p->dx*w1, p->y + p->dy*w1);
			}
			glEnd();
		}
		
		glColor4d(1, 1, 1, 1);
	} else {
		glColor4dv(color);
		glEnable(GL_MULTISAMPLE);
		glBegin(GL_TRIANGLE_STRIP);
		for(int i = 0; i < stroke->count; ++i) {
			HeliStrokePoint *p = stroke->points + i;
			glVertex2d(p->x, p->y);
			glVertex2d(p->x + p->dx*w, p->y + p->dy*w);
		}
		glEnd();
		glDisable(GL_MULTISAMPLE);
		glColor4d(1, 1, 1, 1);
	}
}


static void drawStroke(int close) {
	HeliDrawingState *s = heliDrawingGetState();
	if (s->strokeColor[3] <= HELI_PRECISION) return;
	if (s->strokeWidth < HELI_PRECISION) return;
	if (pathSize < 4) return;
	
	HeliStroke stroke = {};
	stroke.allocatedCount = pathSize;
	stroke.points = calloc(stroke.allocatedCount, sizeof(*stroke.points));
	
	double w = 0.5*s->strokeWidth;
	double precisionSqr = 1/(w*0.75);
	precisionSqr *= precisionSqr;
	
	heliDrawingApplyTexture(s->strokeColor, &s->strokeTexture);
	
	if (close) {
		for(int i = 0; i < pathSize; i += 2) {
			int p = i > 0 ? i - 2 : pathSize - 2;
			int n = i < pathSize - 2 ? i + 2 : 0;
			strokeAddCorner(
				&stroke,
				path[p], path[p+1],
				path[i], path[i+1],
				path[n], path[n+1],
				precisionSqr );
		}
		strokeAddPoint(
			&stroke,
			stroke.points[0].x,
			stroke.points[0].y,
			stroke.points[0].dx,
			stroke.points[0].dy );
		strokeDraw(&stroke, w, s->strokeColor);
		
		stroke.count = 0;
		
		for(int i = pathSize - 2; i >= 0; i -= 2) {
			int p = i < pathSize - 2 ? i + 2 : 0;
			int n = i > 0 ? i - 2 : pathSize - 2;
			strokeAddCorner(
				&stroke,
				path[p], path[p+1],
				path[i], path[i+1],
				path[n], path[n+1],
				precisionSqr );
		}
		strokeAddPoint(
			&stroke,
			stroke.points[0].x,
			stroke.points[0].y,
			stroke.points[0].dx,
			stroke.points[0].dy );
		strokeDraw(&stroke, w, s->strokeColor);
	} else {
		double px = 0, py = 0, nx = 0, ny = 0;
		for(int i = 0; i < pathSize; i += 2) {
			double x = path[i], y = path[i+1];
			if (i > 0)            { px = path[i-2]; py = path[i-1]; }
			if (i < pathSize - 2) { nx = path[i+2]; ny = path[i+3]; }
			if (i == 0)            { px = x + y - ny; py = y + nx - x; }
			if (i == pathSize - 2) { nx = x + py - y; ny = y + x - px; }
			strokeAddCorner(&stroke, px, py, x, y, nx, ny, precisionSqr);
		}
		--stroke.count;
		for(int i = pathSize - 2; i >= 0; i -= 2) {
			double x = path[i], y = path[i+1];
			if (i > 0)            { nx = path[i-2]; ny = path[i-1]; }
			if (i < pathSize - 2) { px = path[i+2]; py = path[i+3]; }
			if (i == 0)            { nx = x + py - y; ny = y + x - px; }
			if (i == pathSize - 2) { px = x + y - ny; py = y + nx - x; }
			strokeAddCorner(&stroke, px, py, x, y, nx, ny, precisionSqr);
		}
		memcpy(stroke.points + stroke.count - 1, stroke.points, sizeof(*stroke.points));
		strokeDraw(&stroke, w, s->strokeColor);
	}
	
	heliDrawingResetTexture();
	free(stroke.points);
}

static void pushPathPoint(double x, double y) {
	if (pathAllocated < pathSize + 2) {
		pathAllocated += pathAllocated/4 + 32;
		path = realloc(path, pathAllocated*sizeof(*path));
		memset(&path[pathSize], 0, (pathAllocated - pathSize)*sizeof(*path));
	}
	path[pathSize++] = x;
	path[pathSize++] = y;
}

static void endPath(int close, int stroke, int fill, int fillSimple) {
	// remove duplicates
	int removed = 0;
	for(int i = 2; i < pathSize; i += 2) {
		if ( fabs(path[i-removed-2] - path[i]) <= 2*HELI_PRECISION
		  && fabs(path[i-removed-1] - path[i+1]) <= 2*HELI_PRECISION )
		{
			removed += 2;
		} else
		if (removed > 0) {
			path[i-removed] = path[i];
			path[i-removed+1] = path[i+1];
		}
	}
	
	// remove tails duplicates
	pathSize -= removed;
	if ( pathSize >= 4
	  && close
	  && fabs(path[pathSize-2] - path[0]) <= 2*HELI_PRECISION
	  && fabs(path[pathSize-1] - path[1]) <= 2*HELI_PRECISION )
		pathSize -= 2;
	
	// draw
	if (pathSize >= 4) {
		if (fillSimple) drawFillSimple();
			else if (fill) drawFill();
		if (stroke) drawStroke(close);
	}
	
	resetPath();
}

void closePath()
	{ endPath(TRUE, TRUE, TRUE, FALSE); }
void strokePath()
	{ endPath(FALSE, TRUE, FALSE, FALSE); }
void lineTo(double x, double y)
	{ pushPathPoint(x, y); }
void moveTo(double x, double y)
	{ resetPath(); lineTo(x, y); }


HeliDrawingState *heliDrawingGetState()
	{ return statesStack + statesStackIndex; }

HeliDrawingState *heliDrawingGetStateStack()
	{ return statesStack; }

void saveState(unsigned int flags)
	{ saveStateEx(STATE_ALL); }

void saveStateEx(unsigned int flags) {
	if (statesStackIndex >= sizeof(statesStack)/sizeof(*statesStack) - 1) {
		fprintf(stderr, "helianthus: drawing stack overflow\n");
		return;
	}
	++statesStackIndex;
	memcpy(statesStack + statesStackIndex, statesStack + statesStackIndex - 1, sizeof(*statesStack));
	statesStack[statesStackIndex].flags = flags;
	heliGLGetCommonState(&statesStack[statesStackIndex].glState, flags);
}

void restoreState() {
	assert(statesStackIndex > 0);
	if (statesStackIndex <= 0) {
		fprintf(stderr, "helianthus: drawing stack underflow\n");
		return;
	}
	
	HeliDrawingState *prev = &statesStack[statesStackIndex];
	heliGLSetCommonState(&prev->glState);
	--statesStackIndex;
	HeliDrawingState *curr = &statesStack[statesStackIndex];
	
	unsigned int flags = ~prev->flags;
	if (flags & STATE_FILL_COLOR)
		memcpy(curr->fillColor, prev->fillColor, sizeof(curr->fillColor));
	if (flags & STATE_FILL_TEXTURE)
		memcpy(&curr->fillTexture, &prev->fillTexture, sizeof(curr->fillTexture));
	if (flags & STATE_STROKE_COLOR)
		memcpy(curr->strokeColor, prev->strokeColor, sizeof(curr->strokeColor));
	if (flags & STATE_STROKE_TEXTURE)
		memcpy(&curr->strokeTexture, &prev->strokeTexture, sizeof(curr->strokeTexture));
	if (flags & STATE_STROKE_WIDTH)
		curr->strokeWidth = prev->strokeWidth;
	if (flags & STATE_TEXT_ALIGN)
		{ curr->horAlign = prev->horAlign; curr->vertAlign = prev->vertAlign; }
	if (flags & STATE_TEXT_SIZE)
		curr->fontSize = prev->fontSize;
	if (flags & STATE_TEXT_FONT)
		curr->font = prev->font;
}

void heliDrawingClearFrame() {
	glClearColor(colorBack[0], colorBack[1], colorBack[2], colorBack[3]);
	glClear(GL_COLOR_BUFFER_BIT);
}

void heliDrawingPrepareFrame() {
	resetPath();
	
	heliDrawingClearFrame();
	glDisable(GL_DEPTH_TEST);
	glDisable(GL_STENCIL_TEST);
	glDisable(GL_CULL_FACE);
	glDisable(GL_MULTISAMPLE);
	
	glEnable(GL_BLEND);
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
	glEnable(GL_LINE_SMOOTH);
	glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
}

void heliDrawingFinish() {
	resetPath();
	free(path);
	path = NULL;
	pathAllocated = 0;
}