src/GLRenderer.cc

changeset 629
b75c6cce02e2
child 632
63e6243b880c
child 675
450827da2376
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/GLRenderer.cc	Tue Jan 21 02:03:27 2014 +0200
@@ -0,0 +1,2225 @@
+/*
+ *  LDForge: LDraw parts authoring CAD
+ *  Copyright (C) 2013, 2014 Santeri Piippo
+ *
+ *  This program is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation, either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <QGLWidget>
+#include <QWheelEvent>
+#include <QMouseEvent>
+#include <QContextMenuEvent>
+#include <QInputDialog>
+#include <QToolTip>
+#include <QTimer>
+#include <GL/glu.h>
+
+#include "Main.h"
+#include "Configuration.h"
+#include "Document.h"
+#include "GLRenderer.h"
+#include "Colors.h"
+#include "MainWindow.h"
+#include "Misc.h"
+#include "EditHistory.h"
+#include "Dialogs.h"
+#include "AddObjectDialog.h"
+#include "MessageLog.h"
+#include "Primitives.h"
+#include "misc/RingFinder.h"
+#include "moc_GLRenderer.cpp"
+
+static const LDFixedCameraInfo g_FixedCameras[6] =
+{
+	{{  1,  0, 0 }, X, Z, false, false },
+	{{  0,  0, 0 }, X, Y, false,  true },
+	{{  0,  1, 0 }, Z, Y,  true,  true },
+	{{ -1,  0, 0 }, X, Z, false,  true },
+	{{  0,  0, 0 }, X, Y,  true,  true },
+	{{  0, -1, 0 }, Z, Y, false,  true },
+};
+
+// Matrix templates for circle drawing. 2 is substituted with
+// the scale value, 1 is inverted to -1 if needed.
+static const Matrix g_circleDrawMatrixTemplates[3] =
+{
+	{ 2, 0, 0, 0, 1, 0, 0, 0, 2 },
+	{ 2, 0, 0, 0, 0, 2, 0, 1, 0 },
+	{ 0, 1, 0, 2, 0, 0, 0, 0, 2 },
+};
+
+cfg (String,	gl_bgcolor,				"#FFFFFF")
+cfg (String,	gl_maincolor,			"#A0A0A0")
+cfg (Float,		gl_maincolor_alpha,		1.0)
+cfg (String,	gl_selectcolor,			"#0080FF")
+cfg (Int,		gl_linethickness,		2)
+cfg (Bool,		gl_colorbfc,			false)
+cfg (Int,		gl_camera,				GLRenderer::EFreeCamera)
+cfg (Bool,		gl_blackedges,			false)
+cfg (Bool,		gl_axes,				false)
+cfg (Bool,		gl_wireframe,			false)
+cfg (Bool,		gl_logostuds,			false)
+cfg (Bool,		gl_aa,					true)
+cfg (Bool,		gl_linelengths,			true)
+cfg (Bool,		gl_drawangles,			false)
+
+// argh
+const char* g_CameraNames[7] =
+{
+	QT_TRANSLATE_NOOP ("GLRenderer",  "Top"),
+	QT_TRANSLATE_NOOP ("GLRenderer",  "Front"),
+	QT_TRANSLATE_NOOP ("GLRenderer",  "Left"),
+	QT_TRANSLATE_NOOP ("GLRenderer",  "Bottom"),
+	QT_TRANSLATE_NOOP ("GLRenderer",  "Back"),
+	QT_TRANSLATE_NOOP ("GLRenderer",  "Right"),
+	QT_TRANSLATE_NOOP ("GLRenderer",  "Free")
+};
+
+const GL::EFixedCamera g_Cameras[7] =
+{
+	GL::ETopCamera,
+	GL::EFrontCamera,
+	GL::ELeftCamera,
+	GL::EBottomCamera,
+	GL::EBackCamera,
+	GL::ERightCamera,
+	GL::EFreeCamera
+};
+
+// Definitions for visual axes, drawn on the screen
+const struct LDGLAxis
+{
+	const QColor col;
+	const Vertex vert;
+} g_GLAxes[3] =
+{
+	{ QColor (255,   0,   0), Vertex (10000, 0, 0) }, // X
+	{ QColor (80,  192,   0), Vertex (0, 10000, 0) }, // Y
+	{ QColor (0,   160, 192), Vertex (0, 0, 10000) }, // Z
+};
+
+static bool g_glInvert = false;
+static QList<int> g_warnedColors;
+
+// =============================================================================
+//
+GLRenderer::GLRenderer (QWidget* parent) : QGLWidget (parent)
+{
+	m_Picking = m_rangepick = false;
+	m_camera = (GL::EFixedCamera) gl_camera;
+	m_drawToolTip = false;
+	m_EditMode = ESelectMode;
+	m_rectdraw = false;
+	m_panning = false;
+	setFile (null);
+	setDrawOnly (false);
+	setMessageLog (null);
+	m_width = m_height = -1;
+	m_hoverpos = g_origin;
+
+	m_toolTipTimer = new QTimer (this);
+	m_toolTipTimer->setSingleShot (true);
+	connect (m_toolTipTimer, SIGNAL (timeout()), this, SLOT (slot_toolTipTimer()));
+
+	m_thickBorderPen = QPen (QColor (0, 0, 0, 208), 2, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin);
+	m_thinBorderPen = m_thickBorderPen;
+	m_thinBorderPen.setWidth (1);
+
+	// Init camera icons
+	for (const GL::EFixedCamera cam : g_Cameras)
+	{
+		QString iconname = fmt ("camera-%1", tr (g_CameraNames[cam]).toLower());
+
+		CameraIcon* info = &m_cameraIcons[cam];
+		info->img = new QPixmap (getIcon (iconname));
+		info->cam = cam;
+	}
+
+	calcCameraIcons();
+}
+
+// =============================================================================
+//
+GLRenderer::~GLRenderer()
+{
+	for (int i = 0; i < 6; ++i)
+		delete currentDocumentData().overlays[i].img;
+
+	for (CameraIcon& info : m_cameraIcons)
+		delete info.img;
+}
+
+// =============================================================================
+// Calculates the "hitboxes" of the camera icons so that we can tell when the
+// cursor is pointing at the camera icon.
+//
+void GLRenderer::calcCameraIcons()
+{
+	int i = 0;
+
+	for (CameraIcon& info : m_cameraIcons)
+	{
+		// MATH
+		const long x1 = (m_width - (info.cam != EFreeCamera ? 48 : 16)) + ((i % 3) * 16) - 1,
+			y1 = ((i / 3) * 16) + 1;
+
+		info.srcRect = QRect (0, 0, 16, 16);
+		info.destRect = QRect (x1, y1, 16, 16);
+		info.selRect = QRect (
+			info.destRect.x(),
+			info.destRect.y(),
+			info.destRect.width() + 1,
+			info.destRect.height() + 1
+		);
+
+		++i;
+	}
+}
+
+// =============================================================================
+//
+void GLRenderer::initGLData()
+{
+	glEnable (GL_BLEND);
+	glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+	glEnable (GL_POLYGON_OFFSET_FILL);
+	glPolygonOffset (1.0f, 1.0f);
+
+	glEnable (GL_DEPTH_TEST);
+	glShadeModel (GL_SMOOTH);
+	glEnable (GL_MULTISAMPLE);
+
+	if (gl_aa)
+	{
+		glEnable (GL_LINE_SMOOTH);
+		glEnable (GL_POLYGON_SMOOTH);
+		glHint (GL_LINE_SMOOTH_HINT, GL_NICEST);
+		glHint (GL_POLYGON_SMOOTH_HINT, GL_NICEST);
+	} else
+	{
+		glDisable (GL_LINE_SMOOTH);
+		glDisable (GL_POLYGON_SMOOTH);
+	}
+}
+
+// =============================================================================
+//
+void GLRenderer::resetAngles()
+{
+	rot (X) = 30.0f;
+	rot (Y) = 325.f;
+	pan (X) = pan (Y) = rot (Z) = 0.0f;
+	zoomToFit();
+}
+
+// =============================================================================
+//
+void GLRenderer::resetAllAngles()
+{
+	EFixedCamera oldcam = camera();
+
+	for (int i = 0; i < 7; ++i)
+	{
+		setCamera ((EFixedCamera) i);
+		resetAngles();
+	}
+
+	setCamera (oldcam);
+}
+
+// =============================================================================
+//
+void GLRenderer::initializeGL()
+{
+	setBackground();
+
+	glLineWidth (gl_linethickness);
+
+	setAutoFillBackground (false);
+	setMouseTracking (true);
+	setFocusPolicy (Qt::WheelFocus);
+	compileAllObjects();
+}
+
+// =============================================================================
+//
+QColor GLRenderer::getMainColor()
+{
+	QColor col (gl_maincolor);
+
+	if (!col.isValid())
+		return QColor (0, 0, 0);
+
+	col.setAlpha (gl_maincolor_alpha * 255.f);
+	return col;
+}
+
+// =============================================================================
+//
+void GLRenderer::setBackground()
+{
+	QColor col (gl_bgcolor);
+
+	if (!col.isValid())
+		return;
+
+	col.setAlpha (255);
+
+	m_darkbg = luma (col) < 80;
+	m_bgcolor = col;
+	qglClearColor (col);
+}
+
+// =============================================================================
+//
+void GLRenderer::setObjectColor (LDObject* obj, const ListType list)
+{
+	QColor qcol;
+
+	if (!obj->isColored())
+		return;
+
+	if (list == GL::PickList)
+	{
+		// Make the color by the object's ID if we're picking, so we can make the
+		// ID again from the color we get from the picking results. Be sure to use
+		// the top level parent's index since we want a subfile's children point
+		// to the subfile itself.
+		long i = obj->topLevelParent()->getID();
+
+		// Calculate a color based from this index. This method caters for
+		// 16777216 objects. I don't think that'll be exceeded anytime soon. :)
+		// ATM biggest is 53588.dat with 12600 lines.
+		double r = (i / 0x10000) % 0x100,
+			   g = (i / 0x100) % 0x100,
+			   b = i % 0x100;
+
+		qglColor (QColor (r, g, b));
+		return;
+	}
+
+	if ((list == BFCFrontList || list == BFCBackList) &&
+		obj->getType() != LDObject::ELine &&
+		obj->getType() != LDObject::ECondLine)
+	{
+		if (list == GL::BFCFrontList)
+			qcol = QColor (40, 192, 0);
+		else
+			qcol = QColor (224, 0, 0);
+	}
+	else
+	{
+		if (obj->getColor() == maincolor)
+			qcol = getMainColor();
+		else
+		{
+			LDColor* col = getColor (obj->getColor());
+
+			if (col)
+				qcol = col->faceColor;
+		}
+
+		if (obj->getColor() == edgecolor)
+		{
+			LDColor* col;
+
+			if (!gl_blackedges && obj->getParent() && (col = getColor (obj->getParent()->getColor())))
+				qcol = col->edgeColor;
+			else
+				qcol = (m_darkbg == false) ? Qt::black : Qt::white;
+		}
+
+		if (qcol.isValid() == false)
+		{
+			// The color was unknown. Use main color to make the object at least
+			// not appear pitch-black.
+			if (obj->getColor() != edgecolor)
+				qcol = getMainColor();
+
+			// Warn about the unknown colors, but only once.
+			for (int i : g_warnedColors)
+				if (obj->getColor() == i)
+					return;
+
+			log ("%1: Unknown color %2!\n", __func__, obj->getColor());
+			g_warnedColors << obj->getColor();
+			return;
+		}
+	}
+
+	int r = qcol.red(),
+		 g = qcol.green(),
+		 b = qcol.blue(),
+		 a = qcol.alpha();
+
+	if (obj->topLevelParent()->isSelected())
+	{
+		// Brighten it up for the select list.
+		QColor selcolor (gl_selectcolor);
+		r = (r + selcolor.red()) / 2;
+		g = (g + selcolor.green()) / 2;
+		b = (b + selcolor.blue()) / 2;
+	}
+
+	glColor4f (
+		((double) r) / 255.0f,
+		((double) g) / 255.0f,
+		((double) b) / 255.0f,
+		((double) a) / 255.0f);
+}
+
+// =============================================================================
+//
+void GLRenderer::refresh()
+{
+	update();
+	swapBuffers();
+}
+
+// =============================================================================
+//
+void GLRenderer::hardRefresh()
+{
+	compileAllObjects();
+	refresh();
+
+	glLineWidth (gl_linethickness);
+}
+
+// =============================================================================
+//
+void GLRenderer::resizeGL (int w, int h)
+{
+	m_width = w;
+	m_height = h;
+
+	calcCameraIcons();
+
+	glViewport (0, 0, w, h);
+	glMatrixMode (GL_PROJECTION);
+	glLoadIdentity();
+	gluPerspective (45.0f, (double) w / (double) h, 1.0f, 10000.0f);
+	glMatrixMode (GL_MODELVIEW);
+}
+
+// =============================================================================
+//
+void GLRenderer::drawGLScene()
+{
+	if (getFile() == null)
+		return;
+
+	if (gl_wireframe && !isPicking())
+		glPolygonMode (GL_FRONT_AND_BACK, GL_LINE);
+
+	glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+	glEnable (GL_DEPTH_TEST);
+
+	if (m_camera != EFreeCamera)
+	{
+		glMatrixMode (GL_PROJECTION);
+		glPushMatrix();
+
+		glLoadIdentity();
+		glOrtho (-m_virtWidth, m_virtWidth, -m_virtHeight, m_virtHeight, -100.0f, 100.0f);
+		glTranslatef (pan (X), pan (Y), 0.0f);
+
+		if (m_camera != EFrontCamera && m_camera != EBackCamera)
+		{
+			glRotatef (90.0f, g_FixedCameras[camera()].glrotate[0],
+				g_FixedCameras[camera()].glrotate[1],
+				g_FixedCameras[camera()].glrotate[2]);
+		}
+
+		// Back camera needs to be handled differently
+		if (m_camera == GLRenderer::EBackCamera)
+		{
+			glRotatef (180.0f, 1.0f, 0.0f, 0.0f);
+			glRotatef (180.0f, 0.0f, 0.0f, 1.0f);
+		}
+	}
+	else
+	{
+		glMatrixMode (GL_MODELVIEW);
+		glPushMatrix();
+		glLoadIdentity();
+
+		glTranslatef (0.0f, 0.0f, -2.0f);
+		glTranslatef (pan (X), pan (Y), -zoom());
+		glRotatef (rot (X), 1.0f, 0.0f, 0.0f);
+		glRotatef (rot (Y), 0.0f, 1.0f, 0.0f);
+		glRotatef (rot (Z), 0.0f, 0.0f, 1.0f);
+	}
+
+	const GL::ListType list = (!isDrawOnly() && isPicking()) ? PickList : NormalList;
+
+	if (gl_colorbfc && !isPicking() && !isDrawOnly())
+	{
+		glEnable (GL_CULL_FACE);
+
+		for (LDObject* obj : getFile()->getObjects())
+		{
+			if (obj->isHidden())
+				continue;
+
+			glCullFace (GL_BACK);
+			glCallList (obj->glLists[BFCFrontList]);
+
+			glCullFace (GL_FRONT);
+			glCallList (obj->glLists[BFCBackList]);
+		}
+
+		glDisable (GL_CULL_FACE);
+	}
+	else
+	{
+		for (LDObject* obj : getFile()->getObjects())
+		{
+			if (obj->isHidden())
+				continue;
+
+			glCallList (obj->glLists[list]);
+		}
+	}
+
+	if (gl_axes && !isPicking() && !isDrawOnly())
+		glCallList (m_axeslist);
+
+	glPopMatrix();
+	glMatrixMode (GL_MODELVIEW);
+	glPolygonMode (GL_FRONT_AND_BACK, GL_FILL);
+}
+
+// =============================================================================
+//
+// This converts a 2D point on the screen to a 3D point in the model. If 'snap'
+// is true, the 3D point will snap to the current grid.
+//
+Vertex GLRenderer::coordconv2_3 (const QPoint& pos2d, bool snap) const
+{
+	assert (camera() != EFreeCamera);
+
+	Vertex pos3d;
+	const LDFixedCameraInfo* cam = &g_FixedCameras[m_camera];
+	const Axis axisX = cam->axisX;
+	const Axis axisY = cam->axisY;
+	const int negXFac = cam->negX ? -1 : 1,
+				negYFac = cam->negY ? -1 : 1;
+
+	// Calculate cx and cy - these are the LDraw unit coords the cursor is at.
+	double cx = (-m_virtWidth + ((2 * pos2d.x() * m_virtWidth) / m_width) - pan (X));
+	double cy = (m_virtHeight - ((2 * pos2d.y() * m_virtHeight) / m_height) - pan (Y));
+
+	if (snap)
+	{
+		cx = Grid::snap (cx, (Grid::Config) axisX);
+		cy = Grid::snap (cy, (Grid::Config) axisY);
+	}
+
+	cx *= negXFac;
+	cy *= negYFac;
+
+	roundToDecimals (cx, 4);
+	roundToDecimals (cy, 4);
+
+	// Create the vertex from the coordinates
+	pos3d[axisX] = cx;
+	pos3d[axisY] = cy;
+	pos3d[3 - axisX - axisY] = getDepthValue();
+	return pos3d;
+}
+
+// =============================================================================
+//
+// Inverse operation for the above - convert a 3D position to a 2D screen
+// position. Don't ask me how this code manages to work, I don't even know.
+//
+QPoint GLRenderer::coordconv3_2 (const Vertex& pos3d) const
+{
+	GLfloat m[16];
+	const LDFixedCameraInfo* cam = &g_FixedCameras[m_camera];
+	const Axis axisX = cam->axisX;
+	const Axis axisY = cam->axisY;
+	const int negXFac = cam->negX ? -1 : 1,
+				negYFac = cam->negY ? -1 : 1;
+
+	glGetFloatv (GL_MODELVIEW_MATRIX, m);
+
+	const double x = pos3d.x();
+	const double y = pos3d.y();
+	const double z = pos3d.z();
+
+	Vertex transformed;
+	transformed[X] = (m[0] * x) + (m[1] * y) + (m[2] * z) + m[3];
+	transformed[Y] = (m[4] * x) + (m[5] * y) + (m[6] * z) + m[7];
+	transformed[Z] = (m[8] * x) + (m[9] * y) + (m[10] * z) + m[11];
+
+	double rx = (((transformed[axisX] * negXFac) + m_virtWidth + pan (X)) * m_width) / (2 * m_virtWidth);
+	double ry = (((transformed[axisY] * negYFac) - m_virtHeight + pan (Y)) * m_height) / (2 * m_virtHeight);
+
+	return QPoint (rx, -ry);
+}
+
+// =============================================================================
+//
+void GLRenderer::paintEvent (QPaintEvent* ev)
+{
+	Q_UNUSED (ev)
+
+	makeCurrent();
+	m_virtWidth = zoom();
+	m_virtHeight = (m_height * m_virtWidth) / m_width;
+
+	initGLData();
+	drawGLScene();
+
+	const QPen textpen (m_darkbg ? Qt::white : Qt::black);
+	const QBrush polybrush (QColor (64, 192, 0, 128));
+	QPainter paint (this);
+	QFontMetrics metrics = QFontMetrics (QFont());
+	paint.setRenderHint (QPainter::HighQualityAntialiasing);
+
+	// If we wish to only draw the brick, stop here
+	if (isDrawOnly())
+		return;
+
+	if (m_camera != EFreeCamera && !isPicking())
+	{
+		// Paint the overlay image if we have one
+		const LDGLOverlay& overlay = currentDocumentData().overlays[m_camera];
+
+		if (overlay.img != null)
+		{
+			QPoint v0 = coordconv3_2 (currentDocumentData().overlays[m_camera].v0),
+					   v1 = coordconv3_2 (currentDocumentData().overlays[m_camera].v1);
+
+			QRect targRect (v0.x(), v0.y(), abs (v1.x() - v0.x()), abs (v1.y() - v0.y())),
+				  srcRect (0, 0, overlay.img->width(), overlay.img->height());
+			paint.drawImage (targRect, *overlay.img, srcRect);
+		}
+
+		// Paint the coordinates onto the screen.
+		QString text = fmt (tr ("X: %1, Y: %2, Z: %3"), m_hoverpos[X], m_hoverpos[Y], m_hoverpos[Z]);
+		QFontMetrics metrics = QFontMetrics (font());
+		QRect textSize = metrics.boundingRect (0, 0, m_width, m_height, Qt::AlignCenter, text);
+		paint.setPen (textpen);
+		paint.drawText (m_width - textSize.width(), m_height - 16, textSize.width(),
+			textSize.height(), Qt::AlignCenter, text);
+
+		QPen linepen = m_thinBorderPen;
+		linepen.setWidth (2);
+		linepen.setColor (luma (m_bgcolor) < 40 ? Qt::white : Qt::black);
+
+		// Mode-specific rendering
+		if (getEditMode() == EDrawMode)
+		{
+			QPoint poly[4];
+			Vertex poly3d[4];
+			int numverts = 4;
+
+			// Calculate polygon data
+			if (!m_rectdraw)
+			{
+				numverts = m_drawedVerts.size() + 1;
+				int i = 0;
+
+				for (Vertex& vert : m_drawedVerts)
+					poly3d[i++] = vert;
+
+				// Draw the cursor vertex as the last one in the list.
+				if (numverts <= 4)
+					poly3d[i] = m_hoverpos;
+				else
+					numverts = 4;
+			}
+			else
+			{
+				// Get vertex information from m_rectverts
+				if (m_drawedVerts.size() > 0)
+					for (int i = 0; i < numverts; ++i)
+						poly3d[i] = m_rectverts[i];
+				else
+					poly3d[0] = m_hoverpos;
+			}
+
+			// Convert to 2D
+			for (int i = 0; i < numverts; ++i)
+				poly[i] = coordconv3_2 (poly3d[i]);
+
+			if (numverts > 0)
+			{
+				// Draw the polygon-to-be
+				paint.setBrush (polybrush);
+				paint.drawPolygon (poly, numverts);
+
+				// Draw vertex blips
+				for (int i = 0; i < numverts; ++i)
+				{
+					QPoint& blip = poly[i];
+					paint.setPen (linepen);
+					drawBlip (paint, blip);
+
+					// Draw their coordinates
+					paint.setPen (textpen);
+					paint.drawText (blip.x(), blip.y() - 8, poly3d[i].toString (true));
+				}
+
+				// Draw line lenghts and angle info if appropriate
+				if (numverts >= 2)
+				{
+					int numlines = (m_drawedVerts.size() == 1) ? 1 : m_drawedVerts.size() + 1;
+					paint.setPen (textpen);
+
+					for (int i = 0; i < numlines; ++i)
+					{
+						const int j = (i + 1 < numverts) ? i + 1 : 0;
+						const int h = (i - 1 >= 0) ? i - 1 : numverts - 1;
+
+						if (gl_linelengths)
+						{
+							const QString label = QString::number (poly3d[i].distanceTo (poly3d[j]));
+							QPoint origin = QLineF (poly[i], poly[j]).pointAt (0.5).toPoint();
+							paint.drawText (origin, label);
+						}
+
+						if (gl_drawangles)
+						{
+							QLineF l0 (poly[h], poly[i]),
+								l1 (poly[i], poly[j]);
+
+							double angle = 180 - l0.angleTo (l1);
+
+							if (angle < 0)
+								angle = 180 - l1.angleTo (l0);
+
+							QString label = QString::number (angle) + QString::fromUtf8 (QByteArray ("\302\260"));
+							QPoint pos = poly[i];
+							pos.setY (pos.y() + metrics.height());
+
+							paint.drawText (pos, label);
+						}
+					}
+				}
+			}
+		}
+		elif (getEditMode() == ECircleMode)
+		{
+			// If we have not specified the center point of the circle yet, preview it on the screen.
+			if (m_drawedVerts.isEmpty())
+				drawBlip (paint, coordconv3_2 (m_hoverpos));
+			else
+			{
+				QVector<Vertex> verts, verts2;
+				const double dist0 = getCircleDrawDist (0),
+					dist1 = (m_drawedVerts.size() >= 2) ? getCircleDrawDist (1) : -1;
+				const int segs = lores;
+				const double angleUnit = (2 * pi) / segs;
+				Axis relX, relY;
+				QVector<QPoint> ringpoints, circlepoints, circle2points;
+
+				getRelativeAxes (relX, relY);
+
+				// Calculate the preview positions of vertices
+				for (int i = 0; i < segs; ++i)
+				{
+					Vertex v = g_origin;
+					v[relX] = m_drawedVerts[0][relX] + (cos (i * angleUnit) * dist0);
+					v[relY] = m_drawedVerts[0][relY] + (sin (i * angleUnit) * dist0);
+					verts << v;
+
+					if (dist1 != -1)
+					{
+						v[relX] = m_drawedVerts[0][relX] + (cos (i * angleUnit) * dist1);
+						v[relY] = m_drawedVerts[0][relY] + (sin (i * angleUnit) * dist1);
+						verts2 << v;
+					}
+				}
+
+				int i = 0;
+				for (const Vertex& v : verts + verts2)
+				{
+					// Calculate the 2D point of the vertex
+					QPoint point = coordconv3_2 (v);
+
+					// Draw a green blip at where it is
+					drawBlip (paint, point);
+
+					// Add it to the list of points for the green ring fill.
+					ringpoints << point;
+
+					// Also add the circle points to separate lists
+					if (i < verts.size())
+						circlepoints << point;
+					else
+						circle2points << point;
+
+					++i;
+				}
+
+				// Insert the first point as the seventeenth one so that
+				// the ring polygon is closed properly.
+				if (ringpoints.size() >= 16)
+					ringpoints.insert (16, ringpoints[0]);
+
+				// Same for the outer ring. Note that the indices are offset by 1
+				// because of the insertion done above bumps the values.
+				if (ringpoints.size() >= 33)
+					ringpoints.insert (33, ringpoints[17]);
+
+				// Draw the ring
+				paint.setBrush ((m_drawedVerts.size() >= 2) ? polybrush : Qt::NoBrush);
+				paint.setPen (Qt::NoPen);
+				paint.drawPolygon (QPolygon (ringpoints));
+
+				// Draw the circles
+				paint.setBrush (Qt::NoBrush);
+				paint.setPen (linepen);
+				paint.drawPolygon (QPolygon (circlepoints));
+				paint.drawPolygon (QPolygon (circle2points));
+
+				{ // Draw the current radius in the middle of the circle.
+					QPoint origin = coordconv3_2 (m_drawedVerts[0]);
+					QString label = QString::number (dist0);
+					paint.setPen (textpen);
+					paint.drawText (origin.x() - (metrics.width (label) / 2), origin.y(), label);
+
+					if (m_drawedVerts.size() >= 2)
+					{
+						label = QString::number (dist1);
+						paint.drawText (origin.x() - (metrics.width (label) / 2), origin.y() + metrics.height(), label);
+					}
+				}
+			}
+		}
+	}
+
+	// Camera icons
+	if (!isPicking())
+	{
+		// Draw a background for the selected camera
+		paint.setPen (m_thinBorderPen);
+		paint.setBrush (QBrush (QColor (0, 128, 160, 128)));
+		paint.drawRect (m_cameraIcons[camera()].selRect);
+
+		// Draw the actual icons
+		for (CameraIcon& info : m_cameraIcons)
+		{
+			// Don't draw the free camera icon when in draw mode
+			if (&info == &m_cameraIcons[GL::EFreeCamera] && getEditMode() != ESelectMode)
+				continue;
+
+			paint.drawPixmap (info.destRect, *info.img, info.srcRect);
+		}
+
+		QString fmtstr = tr ("%1 Camera");
+
+		// Draw a label for the current camera in the bottom left corner
+		{
+			const int margin = 4;
+
+			QString label;
+			label = fmt (fmtstr, tr (g_CameraNames[camera()]));
+			paint.setPen (textpen);
+			paint.drawText (QPoint (margin, height() - (margin + metrics.descent())), label);
+		}
+
+		// Tool tips
+		if (m_drawToolTip)
+		{
+			if (m_cameraIcons[m_toolTipCamera].destRect.contains (m_pos) == false)
+				m_drawToolTip = false;
+			else
+			{
+				QString label = fmt (fmtstr, tr (g_CameraNames[m_toolTipCamera]));
+				QToolTip::showText (m_globalpos, label);
+			}
+		}
+	}
+
+	// Message log
+	if (getMessageLog())
+	{
+		int y = 0;
+		const int margin = 2;
+		QColor penColor = textpen.color();
+
+		for (const MessageManager::Line& line : getMessageLog()->getLines())
+		{
+			penColor.setAlphaF (line.alpha);
+			paint.setPen (penColor);
+			paint.drawText (QPoint (margin, y + margin + metrics.ascent()), line.text);
+			y += metrics.height();
+		}
+	}
+
+	// If we're range-picking, draw a rectangle encompassing the selection area.
+	if (m_rangepick && !isPicking() && m_totalmove >= 10)
+	{
+		int x0 = m_rangeStart.x(),
+			y0 = m_rangeStart.y(),
+			x1 = m_pos.x(),
+			y1 = m_pos.y();
+
+		QRect rect (x0, y0, x1 - x0, y1 - y0);
+		QColor fillColor = (m_addpick ? "#40FF00" : "#00CCFF");
+		fillColor.setAlphaF (0.2f);
+
+		paint.setPen (m_thickBorderPen);
+		paint.setBrush (QBrush (fillColor));
+		paint.drawRect (rect);
+	}
+}
+
+// =============================================================================
+//
+void GLRenderer::drawBlip (QPainter& paint, QPoint pos) const
+{
+	QPen pen = m_thinBorderPen;
+	const int blipsize = 8;
+	pen.setWidth (1);
+	paint.setPen (pen);
+	paint.setBrush (QColor (64, 192, 0));
+	paint.drawEllipse (pos.x() - blipsize / 2, pos.y() - blipsize / 2, blipsize, blipsize);
+}
+
+// =============================================================================
+//
+void GLRenderer::compileAllObjects()
+{
+	if (!getFile())
+		return;
+
+	// Compiling all is a big job, use a busy cursor
+	setCursor (Qt::BusyCursor);
+
+	m_knownVerts.clear();
+
+	for (LDObject* obj : getFile()->getObjects())
+		compileObject (obj);
+
+	// Compile axes
+	glDeleteLists (m_axeslist, 1);
+	m_axeslist = glGenLists (1);
+	glNewList (m_axeslist, GL_COMPILE);
+	glBegin (GL_LINES);
+
+	for (const LDGLAxis& ax : g_GLAxes)
+	{
+		qglColor (ax.col);
+		compileVertex (ax.vert);
+		compileVertex (-ax.vert);
+	}
+
+	glEnd();
+	glEndList();
+
+	setCursor (Qt::ArrowCursor);
+}
+
+// =============================================================================
+//
+void GLRenderer::compileSubObject (LDObject* obj, const GLenum gltype)
+{
+	glBegin (gltype);
+
+	const int numverts = (obj->getType() != LDObject::ECondLine) ? obj->vertices() : 2;
+
+	if (g_glInvert == false)
+		for (int i = 0; i < numverts; ++i)
+			compileVertex (obj->getVertex (i));
+	else
+		for (int i = numverts - 1; i >= 0; --i)
+			compileVertex (obj->getVertex (i));
+
+	glEnd();
+}
+
+// =============================================================================
+//
+void GLRenderer::compileList (LDObject* obj, const GLRenderer::ListType list)
+{
+	setObjectColor (obj, list);
+
+	switch (obj->getType())
+	{
+		case LDObject::ELine:
+		{
+			compileSubObject (obj, GL_LINES);
+		} break;
+
+		case LDObject::ECondLine:
+		{
+			// Draw conditional lines with a dash pattern - however, use a full
+			// line when drawing a pick list to make selecting them easier.
+			if (list != GL::PickList)
+			{
+				glLineStipple (1, 0x6666);
+				glEnable (GL_LINE_STIPPLE);
+			}
+
+			compileSubObject (obj, GL_LINES);
+
+			glDisable (GL_LINE_STIPPLE);
+		} break;
+
+		case LDObject::ETriangle:
+		{
+			compileSubObject (obj, GL_TRIANGLES);
+		} break;
+
+		case LDObject::EQuad:
+		{
+			compileSubObject (obj, GL_QUADS);
+		} break;
+
+		case LDObject::ESubfile:
+		{
+			LDSubfile* ref = static_cast<LDSubfile*> (obj);
+			LDObjectList objs;
+
+			objs = ref->inlineContents (LDSubfile::DeepCacheInline | LDSubfile::RendererInline);
+			bool oldinvert = g_glInvert;
+
+			if (ref->getTransform().getDeterminant() < 0)
+				g_glInvert = !g_glInvert;
+
+			LDObject* prev = ref->prev();
+
+			if (prev && prev->getType() == LDObject::EBFC && static_cast<LDBFC*> (prev)->type == LDBFC::InvertNext)
+				g_glInvert = !g_glInvert;
+
+			for (LDObject* obj : objs)
+			{
+				compileList (obj, list);
+				obj->deleteSelf();
+			}
+
+			g_glInvert = oldinvert;
+		} break;
+
+		default:
+			break;
+	}
+}
+
+// =============================================================================
+//
+void GLRenderer::compileVertex (const Vertex& vrt)
+{
+	glVertex3d (vrt[X], -vrt[Y], -vrt[Z]);
+}
+
+// =============================================================================
+//
+void GLRenderer::clampAngle (double& angle) const
+{
+	while (angle < 0)
+		angle += 360.0;
+
+	while (angle > 360.0)
+		angle -= 360.0;
+}
+
+// =============================================================================
+//
+void GLRenderer::addDrawnVertex (Vertex pos)
+{
+	// If we picked an already-existing vertex, stop drawing
+	if (getEditMode() == EDrawMode)
+	{
+		for (Vertex& vert : m_drawedVerts)
+		{
+			if (vert == pos)
+			{
+				endDraw (true);
+				return;
+			}
+		}
+	}
+
+	m_drawedVerts << pos;
+}
+
+// =============================================================================
+//
+void GLRenderer::mouseReleaseEvent (QMouseEvent* ev)
+{
+	const bool wasLeft = (m_lastButtons & Qt::LeftButton) && ! (ev->buttons() & Qt::LeftButton),
+				   wasRight = (m_lastButtons & Qt::RightButton) && ! (ev->buttons() & Qt::RightButton),
+				   wasMid = (m_lastButtons & Qt::MidButton) && ! (ev->buttons() & Qt::MidButton);
+
+	if (m_panning)
+		m_panning = false;
+
+	if (wasLeft)
+	{
+		// Check if we selected a camera icon
+		if (!m_rangepick)
+		{
+			for (CameraIcon & info : m_cameraIcons)
+			{
+				if (info.destRect.contains (ev->pos()))
+				{
+					setCamera (info.cam);
+					goto end;
+				}
+			}
+		}
+
+		switch (getEditMode())
+		{
+			case EDrawMode:
+			{
+				if (m_rectdraw)
+				{
+					if (m_drawedVerts.size() == 2)
+					{
+						endDraw (true);
+						return;
+					}
+				} else
+				{
+					// If we have 4 verts, stop drawing.
+					if (m_drawedVerts.size() >= 4)
+					{
+						endDraw (true);
+						return;
+					}
+
+					if (m_drawedVerts.isEmpty() && ev->modifiers() & Qt::ShiftModifier)
+					{
+						m_rectdraw = true;
+						updateRectVerts();
+					}
+				}
+
+				addDrawnVertex (m_hoverpos);
+			} break;
+
+			case ECircleMode:
+			{
+				if (m_drawedVerts.size() == 3)
+				{
+					endDraw (true);
+					return;
+				}
+
+				addDrawnVertex (m_hoverpos);
+			} break;
+
+			case ESelectMode:
+			{
+				if (!isDrawOnly())
+				{
+					if (m_totalmove < 10)
+						m_rangepick = false;
+
+					if (!m_rangepick)
+						m_addpick = (m_keymods & Qt::ControlModifier);
+
+					if (m_totalmove < 10 || m_rangepick)
+						pick (ev->x(), ev->y());
+				}
+			} break;
+		}
+
+		m_rangepick = false;
+	}
+
+	if (wasMid && getEditMode() != ESelectMode && m_drawedVerts.size() < 4 && m_totalmove < 10)
+	{
+		// Find the closest vertex to our cursor
+		double mindist = 1024.0f;
+		Vertex closest;
+		bool valid = false;
+
+		QPoint curspos = coordconv3_2 (m_hoverpos);
+
+		for (const Vertex& pos3d: m_knownVerts)
+		{
+			QPoint pos2d = coordconv3_2 (pos3d);
+
+			// Measure squared distance
+			const double dx = abs (pos2d.x() - curspos.x()),
+						 dy = abs (pos2d.y() - curspos.y()),
+						 distsq = (dx * dx) + (dy * dy);
+
+			if (distsq >= 1024.0f) // 32.0f ** 2
+				continue; // too far away
+
+			if (distsq < mindist)
+			{
+				mindist = distsq;
+				closest = pos3d;
+				valid = true;
+
+				// If it's only 4 pixels away, I think we found our vertex now.
+				if (distsq <= 16.0f) // 4.0f ** 2
+					break;
+			}
+		}
+
+		if (valid)
+			addDrawnVertex (closest);
+	}
+
+	if (wasRight && !m_drawedVerts.isEmpty())
+	{
+		// Remove the last vertex
+		m_drawedVerts.removeLast();
+
+		if (m_drawedVerts.isEmpty())
+			m_rectdraw = false;
+	}
+
+end:
+	update();
+	m_totalmove = 0;
+}
+
+// =============================================================================
+//
+void GLRenderer::mousePressEvent (QMouseEvent* ev)
+{
+	m_totalmove = 0;
+
+	if (ev->modifiers() & Qt::ControlModifier)
+	{
+		m_rangepick = true;
+		m_rangeStart.setX (ev->x());
+		m_rangeStart.setY (ev->y());
+		m_addpick = (m_keymods & Qt::AltModifier);
+		ev->accept();
+	}
+
+	m_lastButtons = ev->buttons();
+}
+
+// =============================================================================
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+// =============================================================================
+void GLRenderer::mouseMoveEvent (QMouseEvent* ev)
+{
+	int dx = ev->x() - m_pos.x();
+	int dy = ev->y() - m_pos.y();
+	m_totalmove += abs (dx) + abs (dy);
+
+	const bool left = ev->buttons() & Qt::LeftButton,
+			   mid = ev->buttons() & Qt::MidButton,
+			   shift = ev->modifiers() & Qt::ShiftModifier;
+
+	if (mid || (left && shift))
+	{
+		pan (X) += 0.03f * dx * (zoom() / 7.5f);
+		pan (Y) -= 0.03f * dy * (zoom() / 7.5f);
+		m_panning = true;
+	} elif (left && !m_rangepick && camera() == EFreeCamera)
+	{
+		rot (X) = rot (X) + dy;
+		rot (Y) = rot (Y) + dx;
+
+		clampAngle (rot (X));
+		clampAngle (rot (Y));
+	}
+
+	// Start the tool tip timer
+	if (!m_drawToolTip)
+		m_toolTipTimer->start (500);
+
+	// Update 2d position
+	m_pos = ev->pos();
+	m_globalpos = ev->globalPos();
+
+	// Calculate 3d position of the cursor
+	m_hoverpos = (camera() != EFreeCamera) ? coordconv2_3 (m_pos, true) : g_origin;
+
+	// Update rect vertices since m_hoverpos may have changed
+	updateRectVerts();
+
+	update();
+}
+
+// =============================================================================
+//
+void GLRenderer::keyPressEvent (QKeyEvent* ev)
+{
+	m_keymods = ev->modifiers();
+}
+
+// =============================================================================
+//
+void GLRenderer::keyReleaseEvent (QKeyEvent* ev)
+{
+	m_keymods = ev->modifiers();
+}
+
+// =============================================================================
+//
+void GLRenderer::wheelEvent (QWheelEvent* ev)
+{
+	makeCurrent();
+
+	zoomNotch (ev->delta() > 0);
+	zoom() = clamp (zoom(), 0.01, 10000.0);
+
+	update();
+	ev->accept();
+}
+
+// =============================================================================
+//
+void GLRenderer::leaveEvent (QEvent* ev)
+{
+	(void) ev;
+	m_drawToolTip = false;
+	m_toolTipTimer->stop();
+	update();
+}
+
+// =============================================================================
+//
+void GLRenderer::contextMenuEvent (QContextMenuEvent* ev)
+{
+	g_win->spawnContextMenu (ev->globalPos());
+}
+
+// =============================================================================
+//
+void GLRenderer::setCamera (const GLRenderer::EFixedCamera cam)
+{
+	m_camera = cam;
+	gl_camera = (int) cam;
+	g_win->updateEditModeActions();
+}
+
+// =============================================================================
+//
+void GLRenderer::pick (int mouseX, int mouseY)
+{
+	makeCurrent();
+
+	// Use particularly thick lines while picking ease up selecting lines.
+	glLineWidth (max<double> (gl_linethickness, 6.5f));
+
+	// Clear the selection if we do not wish to add to it.
+	if (!m_addpick)
+	{
+		LDObjectList oldsel = selection();
+		getCurrentDocument()->clearSelection();
+
+		for (LDObject* obj : oldsel)
+			compileObject (obj);
+	}
+
+	setPicking (true);
+
+	// Paint the picking scene
+	glDisable (GL_DITHER);
+	glClearColor (1.0f, 1.0f, 1.0f, 1.0f);
+	drawGLScene();
+
+	int x0 = mouseX,
+		  y0 = mouseY;
+	int x1, y1;
+
+	// Determine how big an area to read - with range picking, we pick by
+	// the area given, with single pixel picking, we use an 1 x 1 area.
+	if (m_rangepick)
+	{
+		x1 = m_rangeStart.x();
+		y1 = m_rangeStart.y();
+	}
+	else
+	{
+		x1 = x0 + 1;
+		y1 = y0 + 1;
+	}
+
+	// x0 and y0 must be less than x1 and y1, respectively.
+	if (x0 > x1)
+		qSwap (x0, x1);
+
+	if (y0 > y1)
+		qSwap (y0, y1);
+
+	// Clamp the values to ensure they're within bounds
+	x0 = max (0, x0);
+	y0 = max (0, y0);
+	x1 = min (x1, m_width);
+	y1 = min (y1, m_height);
+	const int areawidth = (x1 - x0);
+	const int areaheight = (y1 - y0);
+	const qint32 numpixels = areawidth * areaheight;
+
+	// Allocate space for the pixel data.
+	uchar* const pixeldata = new uchar[4 * numpixels];
+	uchar* pixelptr = &pixeldata[0];
+
+	// Read pixels from the color buffer.
+	glReadPixels (x0, m_height - y1, areawidth, areaheight, GL_RGBA, GL_UNSIGNED_BYTE, pixeldata);
+
+	LDObject* removedObj = null;
+
+	// Go through each pixel read and add them to the selection.
+	for (qint32 i = 0; i < numpixels; ++i)
+	{
+		qint32 idx =
+			(*(pixelptr + 0) * 0x10000) +
+			(*(pixelptr + 1) * 0x00100) +
+			(*(pixelptr + 2) * 0x00001);
+		pixelptr += 4;
+
+		if (idx == 0xFFFFFF)
+			continue; // White is background; skip
+
+		LDObject* obj = LDObject::fromID (idx);
+		assert (obj != null);
+
+		// If this is an additive single pick and the object is currently selected,
+		// we remove it from selection instead.
+		if (!m_rangepick && m_addpick)
+		{
+			if (obj->isSelected())
+			{
+				obj->unselect();
+				removedObj = obj;
+				break;
+			}
+		}
+
+		obj->select();
+	}
+
+	delete[] pixeldata;
+
+	// Update everything now.
+	g_win->updateSelection();
+
+	// Recompile the objects now to update their color
+	for (LDObject* obj : selection())
+		compileObject (obj);
+
+	if (removedObj)
+		compileObject (removedObj);
+
+	// Restore line thickness
+	glLineWidth (gl_linethickness);
+
+	setPicking (false);
+	m_rangepick = false;
+	glEnable (GL_DITHER);
+
+	setBackground();
+	repaint();
+}
+
+// =============================================================================
+//
+void GLRenderer::setEditMode (EditMode const& a)
+{
+	m_EditMode = a;
+
+	switch (a)
+	{
+		case ESelectMode:
+		{
+			unsetCursor();
+			setContextMenuPolicy (Qt::DefaultContextMenu);
+		} break;
+
+		case EDrawMode:
+		case ECircleMode:
+		{
+			// Cannot draw into the free camera - use top instead.
+			if (m_camera == EFreeCamera)
+				setCamera (ETopCamera);
+
+			// Disable the context menu - we need the right mouse button
+			// for removing vertices.
+			setContextMenuPolicy (Qt::NoContextMenu);
+
+			// Use the crosshair cursor when drawing.
+			setCursor (Qt::CrossCursor);
+
+			// Clear the selection when beginning to draw.
+			LDObjectList priorsel = selection();
+			getCurrentDocument()->clearSelection();
+
+			for (LDObject* obj : priorsel)
+				compileObject (obj);
+
+			g_win->updateSelection();
+			m_drawedVerts.clear();
+		} break;
+	}
+
+	g_win->updateEditModeActions();
+	update();
+}
+
+// =============================================================================
+//
+void GLRenderer::setFile (LDDocument* const& a)
+{
+	m_File = a;
+
+	if (a != null)
+	{
+		initOverlaysFromObjects();
+
+		if (currentDocumentData().init == false)
+		{
+			resetAllAngles();
+			currentDocumentData().init = true;
+		}
+	}
+}
+
+// =============================================================================
+//
+Matrix GLRenderer::getCircleDrawMatrix (double scale)
+{
+	Matrix transform = g_circleDrawMatrixTemplates[camera() % 3];
+
+	for (int i = 0; i < 9; ++i)
+	{
+		if (transform[i] == 2)
+			transform[i] = scale;
+		elif (transform[i] == 1 && camera() >= 3)
+			transform[i] = -1;
+	}
+
+	return transform;
+}
+
+// =============================================================================
+//
+void GLRenderer::endDraw (bool accept)
+{
+	(void) accept;
+
+	// Clean the selection and create the object
+	QList<Vertex>& verts = m_drawedVerts;
+	LDObjectList objs;
+
+	switch (getEditMode())
+	{
+		case EDrawMode:
+		{
+			if (m_rectdraw)
+			{
+				LDQuad* quad = new LDQuad;
+
+				// Copy the vertices from m_rectverts
+				updateRectVerts();
+
+				for (int i = 0; i < quad->vertices(); ++i)
+					quad->setVertex (i, m_rectverts[i]);
+
+				quad->setColor (maincolor);
+				objs << quad;
+			}
+			else
+			{
+				switch (verts.size())
+				{
+					case 1:
+					{
+						// 1 vertex - add a vertex object
+						LDVertex* obj = new LDVertex;
+						obj->pos = verts[0];
+						obj->setColor (maincolor);
+						objs << obj;
+					} break;
+
+					case 2:
+					{
+						// 2 verts - make a line
+						LDLine* obj = new LDLine (verts[0], verts[1]);
+						obj->setColor (edgecolor);
+						objs << obj;
+					} break;
+
+					case 3:
+					case 4:
+					{
+						LDObject* obj = (verts.size() == 3) ?
+							  static_cast<LDObject*> (new LDTriangle) :
+							  static_cast<LDObject*> (new LDQuad);
+
+						obj->setColor (maincolor);
+
+						for (int i = 0; i < obj->vertices(); ++i)
+							obj->setVertex (i, verts[i]);
+
+						objs << obj;
+					} break;
+				}
+			}
+		} break;
+
+		case ECircleMode:
+		{
+			const int segs = lores, divs = lores; // TODO: make customizable
+			double dist0 = getCircleDrawDist (0),
+				dist1 = getCircleDrawDist (1);
+			LDDocument* refFile = null;
+			Matrix transform;
+			bool circleOrDisc = false;
+
+			if (dist1 < dist0)
+				std::swap<double> (dist0, dist1);
+
+			if (dist0 == dist1)
+			{
+				// If the radii are the same, there's no ring space to fill. Use a circle.
+				refFile = ::getDocument ("4-4edge.dat");
+				transform = getCircleDrawMatrix (dist0);
+				circleOrDisc = true;
+			}
+			elif (dist0 == 0 || dist1 == 0)
+			{
+				// If either radii is 0, use a disc.
+				refFile = ::getDocument ("4-4disc.dat");
+				transform = getCircleDrawMatrix ((dist0 != 0) ? dist0 : dist1);
+				circleOrDisc = true;
+			}
+			elif (g_RingFinder (dist0, dist1))
+			{
+				// The ring finder found a solution, use that. Add the component rings to the file.
+				for (const RingFinder::Component& cmp : g_RingFinder.bestSolution()->getComponents())
+				{
+					// Get a ref file for this primitive. If we cannot find it in the
+					// LDraw library, generate it.
+					if ((refFile = ::getDocument (radialFileName (::Ring, lores, lores, cmp.num))) == null)
+					{
+						refFile = generatePrimitive (::Ring, lores, lores, cmp.num);
+						refFile->setImplicit (false);
+					}
+
+					LDSubfile* ref = new LDSubfile;
+					ref->setFileInfo (refFile);
+					ref->setTransform (getCircleDrawMatrix (cmp.scale));
+					ref->setPosition (m_drawedVerts[0]);
+					ref->setColor (maincolor);
+					objs << ref;
+				}
+			}
+			else
+			{
+				// Ring finder failed, last resort: draw the ring with quads
+				QList<QLineF> c0, c1;
+				Axis relX, relY, relZ;
+				getRelativeAxes (relX, relY);
+				relZ = (Axis) (3 - relX - relY);
+				double x0 = m_drawedVerts[0][relX],
+					y0 = m_drawedVerts[0][relY];
+
+				Vertex templ;
+				templ[relX] = x0;
+				templ[relY] = y0;
+				templ[relZ] = getDepthValue();
+
+				// Calculate circle coords
+				makeCircle (segs, divs, dist0, c0);
+				makeCircle (segs, divs, dist1, c1);
+
+				for (int i = 0; i < segs; ++i)
+				{
+					Vertex v0, v1, v2, v3;
+					v0 = v1 = v2 = v3 = templ;
+					v0[relX] += c0[i].x1();
+					v0[relY] += c0[i].y1();
+					v1[relX] += c0[i].x2();
+					v1[relY] += c0[i].y2();
+					v2[relX] += c1[i].x2();
+					v2[relY] += c1[i].y2();
+					v3[relX] += c1[i].x1();
+					v3[relY] += c1[i].y1();
+
+					LDQuad* q = new LDQuad (v0, v1, v2, v3);
+					q->setColor (maincolor);
+
+					// Ensure the quads always are BFC-front towards the camera
+					if (camera() % 3 <= 0)
+						q->invert();
+
+					objs << q;
+				}
+			}
+
+			if (circleOrDisc)
+			{
+				LDSubfile* ref = new LDSubfile;
+				ref->setFileInfo (refFile);
+				ref->setTransform (transform);
+				ref->setPosition (m_drawedVerts[0]);
+				ref->setColor (maincolor);
+				objs << ref;
+			}
+		} break;
+
+		case ESelectMode:
+		{
+			// this shouldn't happen
+			assert (false);
+			return;
+		} break;
+	}
+
+	if (objs.size() > 0)
+	{
+		for (LDObject* obj : objs)
+		{
+			getFile()->addObject (obj);
+			compileObject (obj);
+		}
+
+		g_win->refresh();
+		g_win->endAction();
+	}
+
+	m_drawedVerts.clear();
+	m_rectdraw = false;
+}
+
+// =============================================================================
+//
+double GLRenderer::getCircleDrawDist (int pos) const
+{
+	assert (m_drawedVerts.size() >= pos + 1);
+	const Vertex& v1 = (m_drawedVerts.size() >= pos + 2) ? m_drawedVerts[pos + 1] : m_hoverpos;
+	Axis relX, relY;
+	getRelativeAxes (relX, relY);
+
+	const double dx = m_drawedVerts[0][relX] - v1[relX];
+	const double dy = m_drawedVerts[0][relY] - v1[relY];
+	return sqrt ((dx * dx) + (dy * dy));
+}
+
+// =============================================================================
+//
+void GLRenderer::getRelativeAxes (Axis& relX, Axis& relY) const
+{
+	const LDFixedCameraInfo* cam = &g_FixedCameras[m_camera];
+	relX = cam->axisX;
+	relY = cam->axisY;
+}
+
+// =============================================================================
+//
+static QList<Vertex> getVertices (LDObject* obj)
+{
+	QList<Vertex> verts;
+
+	if (obj->vertices() >= 2)
+	{
+		for (int i = 0; i < obj->vertices(); ++i)
+			verts << obj->getVertex (i);
+	} elif (obj->getType() == LDObject::ESubfile)
+	{
+		LDSubfile* ref = static_cast<LDSubfile*> (obj);
+		LDObjectList objs = ref->inlineContents (LDSubfile::DeepCacheInline);
+
+		for (LDObject* obj : objs)
+		{
+			verts << getVertices (obj);
+			obj->deleteSelf();
+		}
+	}
+
+	return verts;
+}
+
+// =============================================================================
+//
+void GLRenderer::compileObject (LDObject* obj)
+{
+	deleteLists (obj);
+
+	for (const GL::ListType listType : g_glListTypes)
+	{
+		if (isDrawOnly() && listType != GL::NormalList)
+			continue;
+
+		GLuint list = glGenLists (1);
+		glNewList (list, GL_COMPILE);
+
+		obj->glLists[listType] = list;
+		compileList (obj, listType);
+
+		glEndList();
+	}
+
+	// Mark in known vertices of this object
+	QList<Vertex> verts = getVertices (obj);
+	m_knownVerts << verts;
+	removeDuplicates (m_knownVerts);
+
+	obj->setGLInit (true);
+}
+
+// =============================================================================
+//
+uchar* GLRenderer::getScreencap (int& w, int& h)
+{
+	w = m_width;
+	h = m_height;
+	uchar* cap = new uchar[4 * w * h];
+
+	m_screencap = true;
+	update();
+	m_screencap = false;
+
+	// Capture the pixels
+	glReadPixels (0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, cap);
+
+	return cap;
+}
+
+// =============================================================================
+//
+void GLRenderer::slot_toolTipTimer()
+{
+	// We come here if the cursor has stayed in one place for longer than a
+	// a second. Check if we're holding it over a camera icon - if so, draw
+	// a tooltip.
+for (CameraIcon & icon : m_cameraIcons)
+	{
+		if (icon.destRect.contains (m_pos))
+		{
+			m_toolTipCamera = icon.cam;
+			m_drawToolTip = true;
+			update();
+			break;
+		}
+	}
+}
+
+// =============================================================================
+//
+void GLRenderer::deleteLists (LDObject* obj)
+{
+	// Delete the lists but only if they have been initialized
+	if (!obj->isGLInit())
+		return;
+
+	for (const GL::ListType listType : g_glListTypes)
+		glDeleteLists (obj->glLists[listType], 1);
+
+	obj->setGLInit (false);
+}
+
+// =============================================================================
+//
+Axis GLRenderer::getCameraAxis (bool y, GLRenderer::EFixedCamera camid)
+{
+	if (camid == (GL::EFixedCamera) - 1)
+		camid = m_camera;
+
+	const LDFixedCameraInfo* cam = &g_FixedCameras[camid];
+	return (y) ? cam->axisY : cam->axisX;
+}
+
+// =============================================================================
+//
+bool GLRenderer::setupOverlay (EFixedCamera cam, QString file, int x, int y, int w, int h)
+{
+	QImage* img = new QImage (QImage (file).convertToFormat (QImage::Format_ARGB32));
+	LDGLOverlay& info = getOverlay (cam);
+
+	if (img->isNull())
+	{
+		critical (tr ("Failed to load overlay image!"));
+		delete img;
+		return false;
+	}
+
+	delete info.img; // delete the old image
+
+	info.fname = file;
+	info.lw = w;
+	info.lh = h;
+	info.ox = x;
+	info.oy = y;
+	info.img = img;
+
+	if (info.lw == 0)
+		info.lw = (info.lh * img->width()) / img->height();
+	elif (info.lh == 0)
+		info.lh = (info.lw * img->height()) / img->width();
+
+	const Axis x2d = getCameraAxis (false, cam),
+		y2d = getCameraAxis (true, cam);
+	const double negXFac = g_FixedCameras[cam].negX ? -1 : 1,
+		negYFac = g_FixedCameras[cam].negY ? -1 : 1;
+
+	info.v0 = info.v1 = g_origin;
+	info.v0[x2d] = - (info.ox * info.lw * negXFac) / img->width();
+	info.v0[y2d] = (info.oy * info.lh * negYFac) / img->height();
+	info.v1[x2d] = info.v0[x2d] + info.lw;
+	info.v1[y2d] = info.v0[y2d] + info.lh;
+
+	// Set alpha of all pixels to 0.5
+	for (long i = 0; i < img->width(); ++i)
+		for (long j = 0; j < img->height(); ++j)
+		{
+			uint32 pixel = img->pixel (i, j);
+			img->setPixel (i, j, 0x80000000 | (pixel & 0x00FFFFFF));
+		}
+
+	updateOverlayObjects();
+	return true;
+}
+
+// =============================================================================
+//
+void GLRenderer::clearOverlay()
+{
+	if (camera() == EFreeCamera)
+		return;
+
+	LDGLOverlay& info = currentDocumentData().overlays[camera()];
+	delete info.img;
+	info.img = null;
+
+	updateOverlayObjects();
+}
+
+// =============================================================================
+//
+void GLRenderer::setDepthValue (double depth)
+{
+	assert (camera() < EFreeCamera);
+	currentDocumentData().depthValues[camera()] = depth;
+}
+
+// =============================================================================
+//
+double GLRenderer::getDepthValue() const
+{
+	assert (camera() < EFreeCamera);
+	return currentDocumentData().depthValues[camera()];
+}
+
+// =============================================================================
+//
+const char* GLRenderer::getCameraName() const
+{
+	return g_CameraNames[camera()];
+}
+
+// =============================================================================
+//
+LDGLOverlay& GLRenderer::getOverlay (int newcam)
+{
+	return currentDocumentData().overlays[newcam];
+}
+
+// =============================================================================
+//
+void GLRenderer::zoomNotch (bool inward)
+{
+	if (zoom() > 15)
+		zoom() *= inward ? 0.833f : 1.2f;
+	else
+		zoom() += inward ? -1.2f : 1.2f;
+}
+
+// =============================================================================
+//
+void GLRenderer::zoomToFit()
+{
+	if (getFile() == null || m_width == -1 || m_height == -1)
+	{
+		zoom() = 30.0f;
+		return;
+	}
+
+	bool lastfilled = false;
+	bool firstrun = true;
+	const uint32 white = 0xFFFFFFFF;
+	bool inward = true;
+	const int w = m_width, h = m_height;
+	int runaway = 50;
+
+	glClearColor (1.0, 1.0, 1.0, 1.0);
+	glDisable (GL_DITHER);
+
+	// Use the pick list while drawing the scene, this way we can tell whether borders
+	// are background or not.
+	setPicking (true);
+
+	while (--runaway)
+	{
+		if (zoom() > 10000.0 || zoom() < 0.0)
+		{
+			// Obviously, there's nothing to draw if we get here.
+			// Default to 30.0f and break out.
+			zoom() = 30.0;
+			break;
+		}
+
+		zoomNotch (inward);
+
+		uchar* cap = new uchar[4 * w * h];
+		drawGLScene();
+		glReadPixels (0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, cap);
+		uint32* imgdata = reinterpret_cast<uint32*> (cap);
+		bool filled = false;
+
+		// Check the top and bottom rows
+		for (int i = 0; i < w; ++i)
+		{
+			if (imgdata[i] != white || imgdata[((h - 1) * w) + i] != white)
+			{
+				filled = true;
+				goto endOfLoop;
+			}
+		}
+
+		// Left and right edges
+		for (int i = 0; i < h; ++i)
+		{
+			if (imgdata[i * w] != white || imgdata[(i * w) + w - 1] != white)
+			{
+				filled = true;
+				goto endOfLoop;
+			}
+		}
+
+endOfLoop:
+
+		delete[] cap;
+
+		if (firstrun)
+		{
+			// If this is the first run, we don't know enough to determine
+			// whether the zoom was to fit, so we mark in our knowledge so
+			// far and start over.
+			inward = !filled;
+			firstrun = false;
+		}
+		else
+		{
+			// If this run filled the screen and the last one did not, the
+			// last run had ideal zoom - zoom a bit back and we should reach it.
+			if (filled && !lastfilled)
+			{
+				zoomNotch (false);
+				break;
+			}
+
+			// If this run did not fill the screen and the last one did, we've
+			// now reached ideal zoom so we're done here.
+			if (!filled && lastfilled)
+				break;
+
+			inward = !filled;
+		}
+
+		lastfilled = filled;
+	}
+
+	setBackground();
+	setPicking (false);
+}
+
+// =============================================================================
+//
+void GLRenderer::zoomAllToFit()
+{
+	EFixedCamera oldcam = camera();
+
+	for (int i = 0; i < 7; ++i)
+	{
+		setCamera ((EFixedCamera) i);
+		zoomToFit();
+	}
+
+	setCamera (oldcam);
+}
+
+// =============================================================================
+//
+void GLRenderer::updateRectVerts()
+{
+	if (!m_rectdraw)
+		return;
+
+	if (m_drawedVerts.isEmpty())
+	{
+		for (int i = 0; i < 4; ++i)
+			m_rectverts[i] = m_hoverpos;
+
+		return;
+	}
+
+	Vertex v0 = m_drawedVerts[0],
+		   v1 = (m_drawedVerts.size() >= 2) ? m_drawedVerts[1] : m_hoverpos;
+
+	const Axis ax = getCameraAxis (false),
+			   ay = getCameraAxis (true),
+			   az = (Axis) (3 - ax - ay);
+
+	for (int i = 0; i < 4; ++i)
+		m_rectverts[i][az] = getDepthValue();
+
+	m_rectverts[0][ax] = v0[ax];
+	m_rectverts[0][ay] = v0[ay];
+	m_rectverts[1][ax] = v1[ax];
+	m_rectverts[1][ay] = v0[ay];
+	m_rectverts[2][ax] = v1[ax];
+	m_rectverts[2][ay] = v1[ay];
+	m_rectverts[3][ax] = v0[ax];
+	m_rectverts[3][ay] = v1[ay];
+}
+
+// =============================================================================
+//
+void GLRenderer::mouseDoubleClickEvent (QMouseEvent* ev)
+{
+	if (!(ev->buttons() & Qt::LeftButton) || getEditMode() != ESelectMode)
+		return;
+
+	pick (ev->x(), ev->y());
+
+	if (selection().isEmpty())
+		return;
+
+	LDObject* obj = selection().first();
+	AddObjectDialog::staticDialog (obj->getType(), obj);
+	g_win->endAction();
+	ev->accept();
+}
+
+// =============================================================================
+//
+LDOverlay* GLRenderer::findOverlayObject (EFixedCamera cam)
+{
+	LDOverlay* ovlobj = null;
+
+	for (LDObject* obj : getFile()->getObjects())
+	{
+		if (obj->getType() == LDObject::EOverlay && static_cast<LDOverlay*> (obj)->getCamera() == cam)
+		{
+			ovlobj = static_cast<LDOverlay*> (obj);
+			break;
+		}
+	}
+
+	return ovlobj;
+}
+
+// =============================================================================
+//
+// Read in overlays from the current file and update overlay info accordingly.
+//
+void GLRenderer::initOverlaysFromObjects()
+{
+	for (EFixedCamera cam : g_Cameras)
+	{
+		if (cam == EFreeCamera)
+			continue;
+
+		LDGLOverlay& meta = currentDocumentData().overlays[cam];
+		LDOverlay* ovlobj = findOverlayObject (cam);
+
+		if (!ovlobj && meta.img)
+		{
+			delete meta.img;
+			meta.img = null;
+		} elif (ovlobj && (!meta.img || meta.fname != ovlobj->getFileName()))
+			setupOverlay (cam, ovlobj->getFileName(), ovlobj->getX(),
+				ovlobj->getY(), ovlobj->getWidth(), ovlobj->getHeight());
+	}
+}
+
+// =============================================================================
+//
+void GLRenderer::updateOverlayObjects()
+{
+	for (EFixedCamera cam : g_Cameras)
+	{
+		if (cam == EFreeCamera)
+			continue;
+
+		LDGLOverlay& meta = currentDocumentData().overlays[cam];
+		LDOverlay* ovlobj = findOverlayObject (cam);
+
+		if (!meta.img && ovlobj)
+		{
+			// If this is the last overlay image, we need to remove the empty space after it as well.
+			LDObject* nextobj = ovlobj->next();
+
+			if (nextobj && nextobj->getType() == LDObject::EEmpty)
+				nextobj->deleteSelf();
+
+			// If the overlay object was there and the overlay itself is
+			// not, remove the object.
+			ovlobj->deleteSelf();
+		} elif (meta.img && !ovlobj)
+		{
+			// Inverse case: image is there but the overlay object is
+			// not, thus create the object.
+			ovlobj = new LDOverlay;
+
+			// Find a suitable position to place this object. We want to place
+			// this into the header, which is everything up to the first scemantic
+			// object. If we find another overlay object, place this object after
+			// the last one found. Otherwise, place it before the first schemantic
+			// object and put an empty object after it (though don't do this if
+			// there was no schemantic elements at all)
+			int i, lastOverlay = -1;
+			bool found = false;
+
+			for (i = 0; i < getFile()->getObjectCount(); ++i)
+			{
+				LDObject* obj = getFile()->getObject (i);
+
+				if (obj->isScemantic())
+				{
+					found = true;
+					break;
+				}
+
+				if (obj->getType() == LDObject::EOverlay)
+					lastOverlay = i;
+			}
+
+			if (lastOverlay != -1)
+				getFile()->insertObj (lastOverlay + 1, ovlobj);
+			else
+			{
+				getFile()->insertObj (i, ovlobj);
+
+				if (found)
+					getFile()->insertObj (i + 1, new LDEmpty);
+			}
+		}
+
+		if (meta.img && ovlobj)
+		{
+			ovlobj->setCamera (cam);
+			ovlobj->setFileName (meta.fname);
+			ovlobj->setX (meta.ox);
+			ovlobj->setY (meta.oy);
+			ovlobj->setWidth (meta.lw);
+			ovlobj->setHeight (meta.lh);
+		}
+	}
+
+	if (g_win->R() == this)
+		g_win->refresh();
+}

mercurial