#if 0
typedef struct
{
	string	shadername;	//aka: texture name
	vector	planenormal;
	float	planedist;
	vector	sdir;
	float	sbias;
	vector	tdir;
	float	tbias;
} brushface_t;
#define out //for annotation only
#define in //for annotation only
#define inout //for annotation only
int brush_get(float modelid, int brushid, out brushface_t *faces, int maxfaces, int *contents) = #0; //returns number of faces, or 0 on error. 
int brush_create(float modelid, in brushface_t *faces, int numfaces, int contents) = #0; //returns newly created brushid.
void brush_delete(float modelid, int brushid) = #0; //destroys brush
//float brush_editplane(float modelid, int brushid, int faceid, in brushface_t *face) = #0; //allows changing shader/s/t info, but ignores plane changes. returns success
float brush_selected(float modelid, int brushid, int faceid, float selectedstate) = #0; //allows you to easily set visual properties of a brush. if brush/face is -1, applies to all. returns old value. selectedstate=-1 changes nothing (called for its return value).
int(float modelid, int brushid, int faceid, vector *points, int maxpoints) brush_getfacepoints = #0;
int(float modelid, vector *planes, float *dists, int numplanes, int *out_brushes, int *out_faces, int maxresults) brush_findinvolume = #0;
#endif
int trace_brush_id;	//set by traceline
int trace_brush_faceid;
/*traceline/tracebox returns trace_brush_id and trace_brush_faceid


mod should track selected brush list instead of polling.
to move 50 brushes, mod needs to get+delete+transform+create
brush ids are ints. this allows different clients to use different ranges without float problems.
*/

int selectedbrush;
int selectedbrushface;
float selectedbrushmodel;
vector facepoints[64];

//history is implemented using a ringbuffer
typedef struct
{
	float timestamp;
	int brushmodel;
	int wasdelete;
	int id;
	brushface_t *brushdata;
	int numfaces;
	int contents;
} history_t;
nosave history_t historyring[128];
int history_min;	//oldest we can go
int history;		//updated on each change
int history_max;	//max value for redo

void() brush_undo =
{
	do
	{
		if (history <= history_min)
			return;
		history--;

		history_t *h = &historyring[history & (historyring.length-1)];

		//we're undoing, so deletes create and creates delete. weird, yes.
		if (h->wasdelete)
			h->id = brush_create(h->brushmodel, h->brushdata, h->numfaces, h->contents);
		else
		{
			brush_delete(h->brushmodel, h->id);
			h->id = 0;
		}
	} while (historyring[(history-1) & (historyring.length-1)].timestamp == h->timestamp);
};
void() brush_redo =
{
	do
	{
		if (history >= history_max)
			return;

		history_t *h = &historyring[history & (historyring.length-1)];

		//we're redoing stuff that has previously been doing. yay.
		if (h->wasdelete)
		{
			brush_delete(h->brushmodel, h->id);
			h->id = 0;
		}
		else
		{
			h->id = brush_create(h->brushmodel, h->brushdata, h->numfaces, h->contents);
		}
		history++;
	} while (historyring[history & (historyring.length-1)].timestamp == h->timestamp);
};
//create and journal.
int(int mod, brushface_t *faces, int numfaces, int contents) brush_history_create =
{
	history_t *h = &historyring[history & (historyring.length-1)];

	h->timestamp = time;
	memfree(h->brushdata);
	h->brushdata = memalloc(sizeof(brushface_t) * h->numfaces);
	memcpy(h->brushdata, faces, sizeof(brushface_t) * h->numfaces);
	h->numfaces = numfaces;
	h->contents = contents;
	h->brushmodel = mod;
	h->wasdelete = FALSE;

	h->id = brush_create(h->brushmodel, h->brushdata, h->numfaces, h->contents);

	history++;
	history_max = history;	//always break any pending redos
	if (history_min < history_max - historyring.length)
		history_min = history_max - historyring.length;
	return h->id;
};
//delete and journal.
void(int mod, int id) brush_history_delete =
{
	int numfaces = brush_get(mod, id, __NULL__, 0, __NULL__);
	if (!numfaces)
		return;	//doesn't exist or something, some kind of error. we can't log a delete if it didn't delete anything, if only because we can't recreate it if its undone.
	history_t *h = &historyring[history & (historyring.length-1)];

	h->timestamp = time;
	h->brushmodel = mod;
	h->id = 0;
	h->contents = 0;
	h->wasdelete = TRUE;

	memfree(h->brushdata);
	h->brushdata = memalloc(sizeof(brushface_t) * numfaces);
	h->numfaces = brush_get(mod, id, h->brushdata, numfaces, &h->contents);
	brush_delete(mod, id);

	history++;
	history_max = history;	//always break any pending redos
	if (history_min < history_max - historyring.length)
		history_min = history_max - historyring.length;
};

brushface_t tmp_faces[64];
int tmp_numfaces;
int tmp_contents;

//int selected;
int brushlist[64];

var string	autocvar_ca_newbrushtexture = "metal4_2";
var float	autocvar_ca_newbrushheight = 64;
var float	autocvar_ca_brushdisplacement = 16;
var float	autocvar_ca_brushrotation = 45.0/2;
var float	autocvar_ca_grid = 16;

enum
{
	BT_NONE,	//selection
	BT_MOVE,
	BT_MERGE,
	BT_MOVEFACE,
	BT_CREATE,
	BT_CLONEDISPLACE,
	BT_SLICE
};
int brushtool;
int bt_points;
vector bt_point[64];

//basic cube plane normals, for selection.
static nosave const vector axis[6] = {
	'-1 0 0',
	'0 -1 0',
	'0 0 -1',
	'1 0 0',
	'0 1 0',
	'0 0 1'
};
float dists[6];

vector(vector a, vector b) cross =
{
    return [	a_y * b_z - a_z * b_y,
    			a_z * b_x - a_x * b_z, 
    			a_x * b_y - a_y * b_x];
};
#define dot(a,b) ((a)*(b))

vector(vector guess) brush_snappoint =
{
	int facenum, points, point;
	float bestdist = autocvar_ca_grid*autocvar_ca_grid;	//worst case value so we don't snap to grid when there's a vertex 0.001qu away from the grid.
	vector best = guess * (1.0/autocvar_ca_grid);
	best_x = rint(best_x);	//snap to grid
	best_y = rint(best_y);
	best_z = rint(best_z);
	best *= autocvar_ca_grid;

	//find surfaces within 32qu of the point (via plane volume). use a tetrahedron instead if you want something more circular
	for (facenum = 0; facenum < axis.length; facenum++)
		dists[facenum] = (guess * axis[facenum]) + autocvar_ca_grid;
	int brushnum, numbrushes = brush_findinvolume(selectedbrushmodel, &axis[0], &dists[0], 6, &brushlist[0], __NULL__, brushlist.length);

	for (brushnum = 0; brushnum < numbrushes; brushnum++)
	{
		for (facenum = 0; ; )
		{
			points = brush_getfacepoints(selectedbrushmodel, brushlist[brushnum], ++facenum, &facepoints[0], facepoints.length);
			if (!points)
				break;	//end of face list, I guess

			//walk the faces we found and use the point if its nearer than our previous guess.
			for (point = 0; point < points; point++)
			{
				vector disp = facepoints[point] - guess;
				float dist = disp*disp;
				if (dist < bestdist)
					best = facepoints[point];
			}
		}
	}

	return best;
};

void(float brushid) editor_drawbbox =
{
	static vector bbox[2];
	int p = brush_getfacepoints(selectedbrushmodel, brushid, 0, &bbox[0], bbox.length);
	if (p == 2)
	{
		R_BeginPolygon("chop");
#define line(x,y) 			R_PolygonVertex(x, '0 0', '1 0 0', 1);	R_PolygonVertex(y, '0 0', '1 0 0', 1);	R_EndPolygon()
		line(bbox[0], ([bbox[1][0], bbox[0][1], bbox[0][2]]));
		line(bbox[0], ([bbox[0][0], bbox[1][1], bbox[0][2]]));
		line(bbox[0], ([bbox[0][0], bbox[0][1], bbox[1][2]]));
		line(bbox[1], ([bbox[0][0], bbox[1][1], bbox[1][2]]));
		line(bbox[1], ([bbox[1][0], bbox[0][1], bbox[1][2]]));
		line(bbox[1], ([bbox[1][0], bbox[1][1], bbox[0][2]]));

		line(([bbox[1][0], bbox[0][1], bbox[0][2]]), ([bbox[1][0], bbox[1][1], bbox[0][2]]));
		line(([bbox[1][0], bbox[0][1], bbox[0][2]]), ([bbox[1][0], bbox[0][1], bbox[1][2]]));
		line(([bbox[0][0], bbox[1][1], bbox[0][2]]), ([bbox[0][0], bbox[1][1], bbox[1][2]]));
		line(([bbox[0][0], bbox[1][1], bbox[0][2]]), ([bbox[1][0], bbox[1][1], bbox[0][2]]));
		line(([bbox[0][0], bbox[0][1], bbox[1][2]]), ([bbox[0][0], bbox[1][1], bbox[1][2]]));
		line(([bbox[0][0], bbox[0][1], bbox[1][2]]), ([bbox[1][0], bbox[0][1], bbox[1][2]]));
#undef line
	}
};

void(vector mousepos) editor_brushes_add =
{
	vector col = '0 0 0';
	int points, point;
	int facenum = 0;
	float intensity = (sin(gettime(5)*4)+1)*0.05;

	//make sure this shader was generated already.
	shaderforname("terrainedit",
		"{"
			"{\n"
				"map terrainedit\n"
				"blendfunc add\n"
				"rgbgen vertex\n"
				"alphagen vertex\n"
			"}\n"
		"}");

	//make sure this shader was generated already.
	shaderforname("chop",
		"{"
			"cull disable\n"
			"{\n"
				"map terrainedit\n"
				"blendfunc add\n"
				"rgbgen vertex\n"
				"alphagen vertex\n"
				"sort nearest\n"
				"nodepthtest\n"
			"}\n"
		"}");

	//draw the selected brush.
	for(;;)
	{
		points = brush_getfacepoints(selectedbrushmodel, selectedbrush, ++facenum, &facepoints[0], facepoints.length);
		if (!points)
			break;	//end of face list, I guess

		if (facenum == selectedbrushface)
			col = [0,intensity,0];
		else
			col = [intensity,0,0];

		R_BeginPolygon("terrainedit");
		for (point = 0; point < points; point++)
			R_PolygonVertex(facepoints[point], '0 0', col, 1);
		R_EndPolygon();
	}
	editor_drawbbox(selectedbrush);

	vector t = mousefar;
	vector o = mousenear;
	if (vlen(o - t) > 8192)
		t = o + normalize(t)*8192;
	traceline(o, t, TRUE, world);

#if 0
	//find all the brushes within 32qu of the mouse cursor's impact point
	//you can tweak this to find the nearest brush vertex efficiently (or snap it to a grid or so).
	//you can then slice through a brush by generating a plane between three points found this way and inserting it into the brush, clipping away the extra part.
	//(remember, the engine will automatically discard any degenerate planes)
	col = '0 0 1';

	//generate the volume
	for (facenum = 0; facenum < 6; facenum++)
		dists[facenum] = (trace_endpos * axis[facenum]) + 32;

	int brushnum, numbrushes = brush_findinvolume(selectedbrushmodel, axis, dists, dists.length, &brushlist[0], __NULL__, brushlist.length);
	for (brushnum = 0; brushnum < numbrushes; brushnum++)
	{
		for (facenum = 0; ; )
		{
			points = brush_getfacepoints(selectedbrushmodel, brushlist[brushnum], ++facenum, &facepoints[0], facepoints.length);
			if (!points)
				break;	//end of face list, I guess

			R_BeginPolygon("terrainedit");
			for (point = 0; point < points; point++)
				R_PolygonVertex(facepoints[point], '0 0', col, 1);
			R_EndPolygon();
		}
	}
#endif

	//draw a line/triangle to show the selection.
	int showpoints = bt_points;
	if (brushtool == BT_SLICE)
	{
//		bt_point[showpoints++] = brush_snappoint(trace_endpos);
		showpoints = 3;
	}
	if (brushtool == BT_CREATE || brushtool == BT_CLONEDISPLACE)
		bt_point[showpoints++] = brush_snappoint(trace_endpos);

	//FIXME: if slicing, draw the intersection

	if (showpoints)
	{
		col = '0 0 1';

		R_BeginPolygon("chop");
		for (point = 0; point < showpoints; point++)
			R_PolygonVertex(bt_point[point], '0 0', col, 1);
		R_EndPolygon();
	}

	//FIXME: if creating, draw a wireframe brush.
};

void(vector mousepos) editor_brushes_overlay =
{
	switch(brushtool)
	{
	case BT_CLONEDISPLACE:
		drawrawstring('0 32 0', "Clone+Displace", '8 8 0', '1 1 1', 1);
		if (!selectedbrush)
			brushtool = BT_NONE;
		break;
	case BT_CREATE:
		drawrawstring('0 32 0', "Paint+Create", '8 8 0', '1 1 1', 1);
		break;
	case BT_SLICE:
		drawrawstring('0 32 0', "Slice Tool", '8 8 0', '1 1 1', 1);
		if (!selectedbrush)
			brushtool = BT_NONE;
		break;
	}
};

//yay pointers!
//move a brush so that its planes all move without any translations in positions or texcoords
static void brushface_translate(brushface_t *fa, int numfaces, vector displacement)
{
	int i;
	for (i = 0; i < numfaces; i++, fa++)
	{
		fa->planedist += fa->planenormal * displacement;
		fa->sbias -= fa->sdir * displacement;
		fa->tbias -= fa->tdir * displacement;
	}
};

//rotates a list of faces by the current v_* vectors, around the origin.
//translate before+after first in order to deal with pivots.
static void brushface_rotate(brushface_t *fa, int numfaces)
{
	int i;
	for (i = 0; i < numfaces; i++, fa++)
	{
		vector orig = fa->planenormal;
		fa->planenormal[0] = orig * v_forward;
		fa->planenormal[1] = orig * -v_right;	//quake just isn't right...
		fa->planenormal[2] = orig * v_up;

		orig = fa->sdir;
		fa->sdir[0] = orig * v_forward;
		fa->sdir[1] = orig * -v_right;	//quake just isn't right...
		fa->sdir[2] = orig * v_up;

		orig = fa->tdir;
		fa->tdir[0] = orig * v_forward;
		fa->tdir[1] = orig * -v_right;	//quake just isn't right...
		fa->tdir[2] = orig * v_up;
	}
};

//generates default quakeed-style texture mapping for the given surface.
//this sucks for cylinders, but keeps slopes and things easy.
void(brushface_t *fa) reset_texturecoords =
{
	//figure out some default texture coords	
	fa->sdir = '0 0 0';
	fa->sbias = 0;
	fa->tdir = '0 0 0';
	fa->tbias = 0;
	float a=fabs(fa->planenormal[0]),b=fabs(fa->planenormal[1]),c=fabs(fa->planenormal[2]);
	if (a>=b&&a>=c)
		fa->sdir[1] = 1;
	else
		fa->sdir[0] = 1;
	if (c>a&&c>b)
		fa->tdir[1] = -1;
	else
		fa->tdir[2] = -1;
};

void() editor_brushes_registercommands =
{
	registercommand("brushedit_undo");
	registercommand("brushedit_redo");
	registercommand("brushedit_create");
	registercommand("brushedit_clone");
	registercommand("brushedit_slice");
	registercommand("brushedit_matchface");
	registercommand("brushedit_resettexcoords");
	registercommand("brushedit_settexture");
	registercommand("brushedit_rotate");
	registercommand("brushedit_binds");
};
float() editor_brushes_command =
{
	switch(argv(0))
	{
	case "brushedit_undo":
		brush_undo();
		break;
	case "brushedit_redo":
		brush_undo();
		break;
	case "brushedit_create":
		brushtool = BT_CREATE;
		bt_points = 0;
		break;
	case "brushedit_clone":
		brushtool = BT_CLONEDISPLACE;
		bt_points = 0;
		break;
	case "brushedit_slice":
		brushtool = BT_SLICE;
		bt_points = 0;
		break;
	case "brushedit_matchface":
		//FIXME
		brushtool = BT_NONE;
		bt_points = 0;
		break;
	case "brushedit_resettexcoords":
		//FIXME
		brushtool = BT_NONE;
		bt_points = 0;
		break;
	case "brushedit_settexture":
		//FIXME
		brushtool = BT_NONE;
		bt_points = 0;
		break;
	case "brushedit_rotate":
		//FIXME
		brushtool = BT_NONE;
		bt_points = 0;
		break;
	case "brushedit_binds":
		localcmd("conmenu\n");
		localcmd("menubind 0  0 \"Creation Tool\" \"brushedit_create\"\n");
		localcmd("menubind 0  8 \"Clone Tool\" \"brushedit_clone\"\n");
		localcmd("menubind 0 16 \"Slice Tool\" \"brushedit_slice\"\n");
		localcmd("menubind 0 24 \"Undo\" \"brushedit_undo\"\n");
		localcmd("menubind 0 32 \"Redo\" \"brushedit_redo\"\n");
		break;
	default:
		return FALSE;
	}

	//just in case.
	cvar_set("ca_show", "1");
	cvar_set("ca_editormode", ftos(MODE_BRUSHEDIT));
	return TRUE;
};

vector(vector dir) axialize =
{
	vector a;
	a_x = fabs(dir_x);
	a_y = fabs(dir_y);
	a_z = fabs(dir_z);
	if (a_x > a_y && a_x > a_z)
		return (dir_x > 0)?'1 0 0':'-1 0 0';
	if (a_y > a_x && a_y > a_z)
		return (dir_y > 0)?'0 1 0':'0 -1 0';
	return (dir_z > 0)?'0 0 1':'0 0 -1';
};

float(float key, float unic, vector mousepos) editor_brushes_key =
{
	brushface_t *fa;
	brushface_t *infa;
	vector t = mousefar;
	vector o = mousenear;
	vector displacement;
	int i, p;
	if (vlen(o - t) > 8192)
		t = o + normalize(t)*8192;
	if (key == K_ESCAPE)
	{
		if (brushtool)
			brushtool = BT_NONE;
		return TRUE;
	}
	if (key == K_MOUSE1)
	{
		traceline(o, t, TRUE, world);

		if (brushtool == BT_SLICE || brushtool == BT_CREATE || brushtool == BT_CLONEDISPLACE)
		{
			if (brushtool == BT_SLICE && bt_points > 2)
				bt_points = 2;

//FIXME: BT_CREATE is planar. so keep the points planar too.
//FIXME: need a way to move points already placed
//FIXME: create needs to ensure verts are clockwise and convex.
			if (bt_points < bt_point.length)
			{
				bt_point[bt_points] = brush_snappoint(trace_endpos);
				bt_points++;
			}

			if (brushtool == BT_SLICE && bt_points == 1)
			{	//slice makes assumptions about the brush, so that you don't have to move to the other side of it first.
				//it ALWAYS draws 3 points if any are defined, so make sure 2+3 have valid locations once point 1 is defined.
				int majoraxis;
				traceline(o, t, TRUE, world);

				brush_selected(selectedbrushmodel, selectedbrush, -1, FALSE);

				static vector bbox[2];
				p = brush_getfacepoints(selectedbrushmodel, selectedbrush, 0, &bbox[0], bbox.length);
	
				t[0] = fabs(trace_plane_normal[0]);
				t[1] = fabs(trace_plane_normal[1]);
				t[2] = fabs(trace_plane_normal[2]);
				if (t[2] > t[0] && t[2] > t[1]) 
					majoraxis = 2;
				else if (t[1] > t[0])
					majoraxis = 1;
				else
					majoraxis = 0;

				bt_point[1] = bt_point[0];
				bt_point[1][majoraxis] = bbox[trace_plane_normal[majoraxis]<0][majoraxis];
				majoraxis = !majoraxis;
				bt_point[2] = bt_point[0];
				bt_point[2][majoraxis] = bbox[trace_plane_normal[majoraxis]<0][majoraxis];
			}
			if  (brushtool == BT_CLONEDISPLACE && bt_points == 2)
			{
				displacement = bt_point[1] - bt_point[0];
				if (selectedbrush && displacement != '0 0 0')
				{
					bt_points = 0;
					tmp_numfaces = brush_get(selectedbrushmodel, selectedbrush, &tmp_faces[0], tmp_faces.length, &tmp_contents);
					brushface_translate(&tmp_faces[0], tmp_numfaces, displacement);
					selectedbrush = brush_history_create(selectedbrushmodel, &tmp_faces[0], tmp_numfaces, tmp_contents);
				}
			}
		}
		else	if (trace_brush_id != selectedbrush || selectedbrushface != trace_brush_faceid || selectedbrushmodel != trace_ent.modelindex)
		{
			brush_selected(selectedbrushmodel, selectedbrush, -1, FALSE);
			selectedbrush = trace_brush_id;
			selectedbrushface = trace_brush_faceid;
			selectedbrushmodel = trace_ent.modelindex;
			brush_selected(selectedbrushmodel, selectedbrush, trace_brush_faceid, TRUE);
		}
		return TRUE;
	}

	if (key == K_ENTER)
	{
		if (brushtool == BT_CREATE && bt_points >= 3)
		{
			fa = &tmp_faces[0];
			fa->planenormal = normalize(cross(bt_point[2] - bt_point[0], bt_point[1] - bt_point[0]));
			fa->planedist = bt_point[0] * fa->planenormal + autocvar_ca_newbrushheight;
			fa->shadername = autocvar_ca_newbrushtexture;
			reset_texturecoords(fa);
			fa++;

			fa->planenormal = -normalize(cross(bt_point[2] - bt_point[0], bt_point[1] - bt_point[0]));
			fa->planedist = (bt_point[0] * fa->planenormal);
			fa->shadername = autocvar_ca_newbrushtexture;
			reset_texturecoords(fa);
			fa++;
			tmp_numfaces = 2;

			for (p = 0; p < bt_points; p++)
			{
				int n = p + 1i;
				if (n == bt_points)
					n = 0;
				fa->planenormal = normalize(cross(bt_point[p] - bt_point[n], tmp_faces[0].planenormal));
				fa->planedist = bt_point[p] * fa->planenormal;
				fa->shadername = autocvar_ca_newbrushtexture;
				reset_texturecoords(fa);
				fa++;
				tmp_numfaces++;
			}

//			tmp_numfaces = fa - &tmp_faces[0];

			bt_points = 0;

			selectedbrush = brush_history_create(selectedbrushmodel, &tmp_faces[0], tmp_numfaces, 1i);
		}
		else if (brushtool == BT_SLICE)
		{
			//get the current faces
			tmp_numfaces = brush_get(selectedbrushmodel, selectedbrush, &tmp_faces[0], tmp_faces.length-1, &tmp_contents);

			//generate a new face plane
			fa = &tmp_faces[tmp_numfaces];
			fa->planenormal = normalize(cross(bt_point[2] - bt_point[0], bt_point[1] - bt_point[0]));
			fa->planedist = bt_point[0] * fa->planenormal;
			fa->shadername = tmp_faces[0].shadername;	//find a neighbour?

			//make sure its okay
			for (i = 0; i < tmp_numfaces; i++)
			{
				if (tmp_faces[i].planenormal == fa->planenormal && tmp_faces[i].planedist == fa->planedist)
				{
					print("that would be co-planar\n");
					return TRUE;
				}
			}
			bt_points = 0;
			tmp_numfaces++;

			reset_texturecoords(fa);		

			//delete the old one and insert the new
			brush_history_delete(selectedbrushmodel, selectedbrush);
			selectedbrush = brush_history_create(selectedbrushmodel, &tmp_faces[0], tmp_numfaces, tmp_contents);

			//and insert another new one too, because inserting a plane like this generates two fragments and I'm too lazy to work out which is the front and which is the back.
			fa->planenormal *= -1;
			fa->planedist *= -1;
			selectedbrush = brush_history_create(selectedbrushmodel, &tmp_faces[0], tmp_numfaces, tmp_contents);
			selectedbrushface = 0;
		}
		return TRUE;
	}

	if (key == 'p')
	{
		brushtool = BT_CREATE;
		bt_points = 0;
		return TRUE;
	}
	if (key == 'l')
	{
		brushtool = BT_CLONEDISPLACE;
		bt_points = 0;
		return TRUE;
	}

	//begin the cut tool
	if (key == 'o') //I'd use #, but that would cause a problem for americans
	{
		brushtool = BT_SLICE;
		bt_points = 0;
		return TRUE;
	}
	if (key == 'm') //I'd use #, but that would cause a problem for americans
	{
		traceline(o, t, TRUE, world);

		int extrafaces;
		float found = FALSE;

		if (selectedbrush == trace_brush_id)
		{
			print("cannot merge brush with itself\n");
			return TRUE;
		}
		if (!trace_brush_id || !selectedbrush)
		{
			print("no brush targetted\n");
			return TRUE;
		}

		tmp_numfaces = brush_get(selectedbrushmodel, selectedbrush, &tmp_faces[0], tmp_faces.length, &tmp_contents);
		infa = &tmp_faces[tmp_numfaces];
		extrafaces = brush_get(selectedbrushmodel, trace_brush_id, &tmp_faces[tmp_numfaces], tmp_faces.length-tmp_numfaces, &tmp_contents);

		//merge the two sets of planes together.
		for(infa = &tmp_faces[tmp_numfaces]; extrafaces > 0; infa++, extrafaces--)
		{
			for (fa = &tmp_faces[0], i = 0; i < tmp_numfaces; i++, fa++)
			{
//print(sprintf("%v %g vs %v %g\n", infa->planenormal, infa->planedist, fa->planenormal, fa->planedist));
				if (fa->planenormal == -infa->planenormal && fa->planedist == -infa->planedist)
				{
//print(sprintf("inverted (%i)\n", ((tmp_numfaces-i)-1i)));
					//inverted. this is the plane we're merging over, so strip it out
					memcpy(fa, &fa[1], sizeof(brushface_t) * ((tmp_numfaces-i)-1));
					tmp_numfaces--;
					i--;
					fa--;
					found = TRUE;
					break;
				}
				else if (fa->planenormal == infa->planenormal && fa->planedist == infa->planedist)
				{
//print("coplanar\n");
					//coplanar surfaces are considered safe to ignore. we use the selected brush's texturing info
					break;
				}
			}
			if (i == tmp_numfaces)
			{	//okay, this plane is important, merge it into the selected brush.
//print("merge plane\n");
				memcpy(fa, infa, sizeof(brushface_t));
				tmp_numfaces++;
			}
		}
		if (!found)
		{
			print("Brushes do not share a plane\n");

			//try to find a surface to move to match to the given plane
			float val;
			float bestval = -0.707;	//must be within 45 degrees
			int bestface = -1;
			tmp_numfaces = brush_get(selectedbrushmodel, selectedbrush, &tmp_faces[0], tmp_faces.length, &tmp_contents);
			brush_get(selectedbrushmodel, trace_brush_id, &tmp_faces[tmp_numfaces], tmp_faces.length-tmp_numfaces, &tmp_contents);
			infa = &tmp_faces[tmp_numfaces + trace_brush_faceid-1i];
			for (i = 0; i < tmp_numfaces; i++)
			{
				val = tmp_faces[i].planenormal * infa->planenormal;
				if (val < bestval)
				{
					bestval = val;
					bestface = i;
				}
			}
			if (bestface != -1)
			{
				//FIXME: determine volume and reject it if we shrink?
				tmp_faces[bestface].planenormal = -infa->planenormal;
				tmp_faces[bestface].planedist = -infa->planedist;

				brush_history_delete(selectedbrushmodel, selectedbrush);
				selectedbrush = brush_history_create(selectedbrushmodel, &tmp_faces[0], tmp_numfaces, tmp_contents);
			}
			return TRUE;
		}
		//FIXME: verify that no planes got dropped, as this indicates that the region wasn't convex, and we probably just destroyed the entire thing.

		brush_history_delete(selectedbrushmodel, trace_brush_id);
		brush_history_delete(selectedbrushmodel, selectedbrush);

		selectedbrush = brush_history_create(selectedbrushmodel, &tmp_faces[0], tmp_numfaces, tmp_contents);
		selectedbrushface = 0;
		brush_selected(selectedbrushmodel, selectedbrush, -1, FALSE);
		return TRUE;
	}

	if (key == K_DEL || key == K_BACKSPACE)
	{
		if (brushtool)
			brushtool = BT_NONE;
		if (selectedbrush)
		{
			brush_history_delete(selectedbrushmodel, selectedbrush);
			selectedbrush = 0;
		}
		return TRUE;
	}
	displacement = '0 0 0';
	if (key == K_PGUP)
		displacement = autocvar_ca_brushdisplacement * axialize(v_up);
	if (key == K_PGDN)
		displacement = autocvar_ca_brushdisplacement * axialize(-v_up);
	if (key == K_KP_UPARROW)
		displacement = autocvar_ca_brushdisplacement * axialize(v_forward);
	if (key == K_KP_DOWNARROW)
		displacement = autocvar_ca_brushdisplacement * axialize(-v_forward);
	if (key == K_KP_RIGHTARROW)
		displacement = autocvar_ca_brushdisplacement * axialize(v_right);
	if (key == K_KP_LEFTARROW)
		displacement = autocvar_ca_brushdisplacement * axialize(-v_right);
	if (displacement != '0 0 0')
	{
		//this is  just an example to show how to move a brush without hurting texture coords at all.
		//note: due to precision loss, this is probably NOT a good way to move brushes around. cache the original position if possible and express transformations relative to that.
		if (selectedbrush)
		{
			tmp_numfaces = brush_get(selectedbrushmodel, selectedbrush, &tmp_faces[0], tmp_faces.length, &tmp_contents);
			brushface_translate(&tmp_faces[0], tmp_numfaces, displacement);
			brush_history_delete(selectedbrushmodel, selectedbrush);
			selectedbrush = brush_history_create(selectedbrushmodel, &tmp_faces[0], tmp_numfaces, tmp_contents);
		}
		return TRUE;
	}
	if (key == '[' || key == ']')
	{
		//this is  just an example to show how to rotate a brush.
		//note: due to precision loss, this is probably NOT a good way to move brushes around. cache the original position if possible and express transformations relative to that.

		//determine rotation matrix
		if (key == '[')
			makevectors([0, -autocvar_ca_brushrotation, 0]);
		else
			makevectors([0, autocvar_ca_brushrotation, 0]);

		//rotate the selected brush
		if (selectedbrush)
		{
			//find the brush's middle (based on its bbox)
			brush_getfacepoints(selectedbrushmodel, selectedbrush, 0, &displacement, 1);

			//grab the existing brush's planes
			tmp_numfaces = brush_get(selectedbrushmodel, selectedbrush, &tmp_faces[0], tmp_faces.length, &tmp_contents);
			//move it so its pivot is at the origin
			brushface_translate(&tmp_faces[0], tmp_numfaces, -displacement);
			//rotate it
			brushface_rotate(&tmp_faces[0], tmp_numfaces);
			//reposition it around its pivot again
			brushface_translate(&tmp_faces[0], tmp_numfaces, displacement);
			//delete the old version of the brush and insert the newly translated/rotated version of it.
			brush_history_delete(selectedbrushmodel, selectedbrush);
			selectedbrush = brush_history_create(selectedbrushmodel, &tmp_faces[0], tmp_numfaces, tmp_contents);
		}
		return TRUE;
	}

	if (key == K_KP_PLUS || key == K_KP_MINUS)
	{
		//push a plane along its normal. yes, this will deform it if its not a cube, as the adjacent planes will not be changed, allowing them to cut into it differently.
		if (selectedbrush && selectedbrushface)
		{
			fa = &tmp_faces[selectedbrushface-1];
			i = tmp_numfaces = brush_get(selectedbrushmodel, selectedbrush, &tmp_faces[0], tmp_faces.length, &tmp_contents);
			displacement = fa->planenormal;
			if (key == K_KP_MINUS)
				displacement *= -1;
			fa->planedist += fa->planenormal * displacement;
			fa->sbias -= fa->sdir * displacement;
			fa->tbias -= fa->tdir * displacement;
			brush_history_delete(selectedbrushmodel, selectedbrush);
			selectedbrush = brush_history_create(selectedbrushmodel, &tmp_faces[0], tmp_numfaces, tmp_contents);

			tmp_numfaces = brush_get(selectedbrushmodel, selectedbrush, &tmp_faces[0], tmp_faces.length, &tmp_contents);
			if (i != tmp_numfaces)
				selectedbrushface = 0;	//if the engine rejected a plane, then it's because something became invalid. don't edit a different face.
				
		}
		return TRUE;
	}
	if (key == 'u')
	{
		brush_undo();
		return TRUE;
	}
	if (key == 'i')
	{
		brush_redo();
		return TRUE;
	}
/*	if (key == 's')
	{
		//CSG subtraction is actually quite easy...
		//for each brush that intersects us, split it by every single one of our planes that intesect
		//drop the resulting brushes if they contain contain points only within the subtraction region
	}
*/	return FALSE;
};
