src/glrenderer.cpp

changeset 1144
4f226fd97826
parent 1135
8e0691be0b6f
child 1145
02264bf0108d
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/glrenderer.cpp	Tue Feb 14 14:51:04 2017 +0200
@@ -0,0 +1,1007 @@
+/*
+ *  LDForge: LDraw parts authoring CAD
+ *  Copyright (C) 2013 - 2017 Teemu 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/>.
+ */
+
+#define GL_GLEXT_PROTOTYPES
+#include <GL/glu.h>
+#include <GL/glext.h>
+#include <QContextMenuEvent>
+#include <QToolTip>
+#include <QTimer>
+#include <GL/glu.h>
+#include "main.h"
+#include "ldDocument.h"
+#include "glrenderer.h"
+#include "colors.h"
+#include "mainwindow.h"
+#include "miscallenous.h"
+#include "editHistory.h"
+#include "glCompiler.h"
+#include "primitives.h"
+#include "documentmanager.h"
+#include "grid.h"
+
+const QPen GLRenderer::thinBorderPen {QColor {0, 0, 0, 208}, 1, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin};
+
+/*
+ * Constructs a GL renderer.
+ */
+GLRenderer::GLRenderer(const Model* model, QWidget* parent) :
+    QGLWidget {parent},
+    HierarchyElement {parent},
+    m_model {model},
+    m_cameras {
+        {"Top camera", {1,  0, 0, X, Z, false, false, false}}, // top
+        {"Front camera", {0,  0, 1, X, Y, false,  true, false}}, // front
+        {"Left camera", {0,  1, 0, Z, Y,  true,  true, false}}, // left
+        {"Bottom camera", {-1,  0, 0, X, Z, false,  true, true}}, // bottom
+        {"Back camera", {0,  0, -1, X, Y,  true,  true, true}}, // back
+        {"Right camera", {0, -1, 0, Z, Y, false,  true, true}}, // right
+        {"Free camera", GLCamera::FreeCamera}, // free
+    }
+{
+	m_camera = (Camera) m_config->camera();
+	m_compiler = new GLCompiler (this);
+	m_toolTipTimer = new QTimer (this);
+	m_toolTipTimer->setSingleShot (true);
+	setAcceptDrops (true);
+	connect (m_toolTipTimer, SIGNAL (timeout()), this, SLOT (showCameraIconTooltip()));
+	resetAllAngles();
+	m_needZoomToFit = true;
+
+	// Init camera icons
+	for (Camera camera : iterateEnum<Camera>())
+	{
+		const char* cameraIconNames[EnumLimits<Camera>::Count] =
+		{
+		    "camera-top", "camera-front", "camera-left",
+		    "camera-bottom", "camera-back", "camera-right",
+		    "camera-free"
+		};
+
+		CameraIcon* info = &m_cameraIcons[static_cast<int>(camera)];
+		info->image = GetIcon (cameraIconNames[static_cast<int>(camera)]);
+		info->camera = camera;
+	}
+
+	calcCameraIcons();
+}
+
+/*
+ * Cleans up the axes VBOs when the renderer is destroyed.
+ */
+GLRenderer::~GLRenderer()
+{
+	glDeleteBuffers(1, &m_axesVbo);
+	glDeleteBuffers(1, &m_axesColorVbo);
+}
+
+/*
+ * Calculates the camera icon locations.
+ */
+void GLRenderer::calcCameraIcons()
+{
+	int i = 0;
+	const int columns = 3;
+	const int firstAtLastRow = countof(m_cameras) - (countof(m_cameras) % columns);
+
+	for (CameraIcon& cameraIcon : m_cameraIcons)
+	{
+		int row = i / columns;
+		int column = i % columns;
+
+		// Do right-justifying on the last row.
+		if (i >= firstAtLastRow)
+			column += columns - (countof(m_cameras) % columns);
+
+		int x1 = width() - 48 + (column * 16) - 1;
+		int y1 = (row * 16) + 1;
+
+		cameraIcon.sourceRect = {0, 0, 16, 16};
+		cameraIcon.targetRect = {x1, y1, 16, 16};
+		cameraIcon.hitRect = {
+		    cameraIcon.targetRect.x(),
+		    cameraIcon.targetRect.y(),
+		    cameraIcon.targetRect.width() + 1,
+		    cameraIcon.targetRect.height() + 1
+		};
+
+		++i;
+	}
+}
+
+/*
+ * Returns the camera currently in use.
+ */
+GLCamera& GLRenderer::currentCamera()
+{
+	return m_cameras[static_cast<int>(camera())];
+}
+
+/*
+ * Returns the camera currently in use.
+ */
+const GLCamera& GLRenderer::currentCamera() const
+{
+	return m_cameras[static_cast<int>(camera())];
+}
+
+/*
+ * Prepares the GL context for rendering.
+ */
+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 (m_config->antiAliasedLines())
+	{
+		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);
+	}
+}
+
+/*
+ * Returns the object currently highlighted by the cursor.
+ */
+LDObject* GLRenderer::objectAtCursor() const
+{
+	return m_objectAtCursor;
+}
+
+// =============================================================================
+//
+void GLRenderer::needZoomToFit()
+{
+	m_needZoomToFit = true;
+}
+
+// =============================================================================
+//
+void GLRenderer::resetAngles()
+{
+	if (m_initialized)
+	{
+		// Why did I even bother trying to compute this by pen and paper? Let GL figure it out...
+		glMatrixMode(GL_MODELVIEW);
+		glPushMatrix();
+		glLoadIdentity();
+		glRotatef(30, 1, 0, 0);
+		glRotatef(330, 0, 1, 0);
+		glGetFloatv(GL_MODELVIEW_MATRIX, m_rotationMatrix.data());
+		glPopMatrix();
+	}
+	currentCamera().setPanning(0, 0);
+	needZoomToFit();
+}
+
+// =============================================================================
+//
+void GLRenderer::resetAllAngles()
+{
+	Camera oldcam = camera();
+
+	for (int i = 0; i < 7; ++i)
+	{
+		setCamera ((Camera) i);
+		resetAngles();
+	}
+
+	setCamera (oldcam);
+}
+
+// =============================================================================
+//
+void GLRenderer::initializeGL()
+{
+	initializeOpenGLFunctions();
+	setBackground();
+	glLineWidth (m_config->lineThickness());
+	glLineStipple (1, 0x6666);
+	setAutoFillBackground (false);
+	setMouseTracking (true);
+	setFocusPolicy (Qt::WheelFocus);
+	m_compiler->initialize();
+	initializeAxes();
+	initializeLighting();
+	m_initialized = true;
+	// Now that GL is initialized, we can reset angles.
+	resetAllAngles();
+}
+
+void GLRenderer::initializeLighting()
+{
+	GLfloat materialShininess[] = {5.0};
+	GLfloat lightPosition[] = {1.0, 1.0, 1.0, 0.0};
+	GLfloat ambientLightingLevel[] = {0.8, 0.8, 0.8, 1.0};
+	glShadeModel(GL_SMOOTH);
+	glMaterialfv(GL_FRONT, GL_SHININESS, materialShininess);
+	glMaterialfv(GL_FRONT, GL_AMBIENT, ambientLightingLevel);
+	glLightfv(GL_LIGHT0, GL_POSITION, lightPosition);
+	glEnable(GL_LIGHTING);
+	glEnable(GL_LIGHT0);
+	glEnable(GL_COLOR_MATERIAL);
+	glEnable(GL_DEPTH_TEST);
+}
+
+// =============================================================================
+//
+void GLRenderer::initializeAxes()
+{
+	// Definitions for visual axes, drawn on the screen
+	struct
+	{
+		QColor color;
+		Vertex extrema;
+	} axisInfo[3] =
+	{
+	    { QColor {192,  96,  96}, Vertex {10000, 0, 0} }, // X
+	    { QColor {48,  192,  48}, Vertex {0, 10000, 0} }, // Y
+		{ QColor {48,  112, 192}, Vertex {0, 0, 10000} }, // Z
+	};
+
+	float axisdata[18];
+	float colorData[18];
+	memset (axisdata, 0, sizeof axisdata);
+
+	for (int i = 0; i < 3; ++i)
+	{
+		const auto& data = axisInfo[i];
+
+		for (Axis axis : axes)
+		{
+			axisdata[(i * 6) + axis] = data.extrema[axis];
+			axisdata[(i * 6) + 3 + axis] = -data.extrema[axis];
+		}
+
+		int offset = i * 6;
+		colorData[offset + 0] = colorData[offset + 3] = data.color.red();
+		colorData[offset + 1] = colorData[offset + 4] = data.color.green();
+		colorData[offset + 2] = colorData[offset + 5] = data.color.blue();
+	}
+
+	glGenBuffers (1, &m_axesVbo);
+	glBindBuffer (GL_ARRAY_BUFFER, m_axesVbo);
+	glBufferData (GL_ARRAY_BUFFER, sizeof axisdata, axisdata, GL_STATIC_DRAW);
+	glGenBuffers (1, &m_axesColorVbo);
+	glBindBuffer (GL_ARRAY_BUFFER, m_axesColorVbo);
+	glBufferData (GL_ARRAY_BUFFER, sizeof colorData, colorData, GL_STATIC_DRAW);
+	glBindBuffer (GL_ARRAY_BUFFER, 0);
+}
+
+// =============================================================================
+//
+void GLRenderer::setBackground()
+{
+	if (not m_isDrawingSelectionScene)
+	{
+		// Otherwise use the background that the user wants.
+		QColor color = m_config->backgroundColor();
+
+		if (color.isValid())
+		{
+			color.setAlpha(255);
+			m_useDarkBackground = luma(color) < 80;
+			m_backgroundColor = color;
+			qglClearColor(color);
+		}
+	}
+	else
+	{
+		// The picking scene requires a black background.
+		glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
+	}
+}
+
+QColor GLRenderer::backgroundColor() const
+{
+	return m_backgroundColor;
+}
+
+// =============================================================================
+//
+void GLRenderer::refresh()
+{
+	update();
+
+	if (isVisible())
+		swapBuffers();
+}
+
+// =============================================================================
+//
+void GLRenderer::resizeGL (int width, int height)
+{
+	calcCameraIcons();
+	glViewport (0, 0, width, height);
+	glMatrixMode (GL_PROJECTION);
+	glLoadIdentity();
+	gluPerspective (45.0f, (double) width / (double) height, 1.0f, 10000.0f);
+	glMatrixMode (GL_MODELVIEW);
+
+	// Unfortunately Qt does not provide a resized() signal so we have to manually feed the information.
+	for (GLCamera& camera : m_cameras)
+		camera.rendererResized(width, height);
+}
+
+// =============================================================================
+//
+void GLRenderer::drawGLScene()
+{
+	if (m_needZoomToFit)
+	{
+		m_needZoomToFit = false;
+		zoomAllToFit();
+	}
+
+	if (m_config->drawWireframe() and not m_isDrawingSelectionScene)
+		glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
+
+	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+	glEnable(GL_DEPTH_TEST);
+
+	if (m_config->lighting())
+		glEnable(GL_LIGHTING);
+	else
+		glDisable(GL_LIGHTING);
+
+	if (camera() != Camera::Free)
+	{
+		glMatrixMode (GL_PROJECTION);
+		glPushMatrix();
+
+		glLoadIdentity();
+		const QSizeF& virtualSize = currentCamera().virtualSize();
+		glOrtho(-virtualSize.width(), virtualSize.width(), -virtualSize.height(), virtualSize.height(), -100.0f, 100.0f);
+		glTranslatef(panning (X), panning (Y), 0.0f);
+
+		if (camera() != Camera::Front and camera() != Camera::Back)
+			glRotatef(90.0f, currentCamera().glRotate(X), currentCamera().glRotate(Y), 0);
+
+		// Back camera needs to be handled differently
+		if (camera() == Camera::Back)
+		{
+			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(panning (X), panning (Y), -zoom());
+		glMultMatrixf(m_rotationMatrix.constData());
+	}
+
+	glEnableClientState (GL_NORMAL_ARRAY);
+	glEnableClientState (GL_VERTEX_ARRAY);
+	glEnableClientState (GL_COLOR_ARRAY);
+
+	if (m_isDrawingSelectionScene)
+	{
+		drawVbos (VboClass::Triangles, VboSubclass::PickColors, GL_TRIANGLES);
+		drawVbos (VboClass::Quads, VboSubclass::PickColors, GL_QUADS);
+		drawVbos (VboClass::Lines, VboSubclass::PickColors, GL_LINES);
+		drawVbos (VboClass::ConditionalLines, VboSubclass::PickColors, GL_LINES);
+	}
+	else
+	{
+		if (m_config->bfcRedGreenView())
+		{
+			glEnable (GL_CULL_FACE);
+			glCullFace (GL_BACK);
+			drawVbos (VboClass::Triangles, VboSubclass::BfcFrontColors, GL_TRIANGLES);
+			drawVbos (VboClass::Quads, VboSubclass::BfcFrontColors, GL_QUADS);
+			glCullFace (GL_FRONT);
+			drawVbos (VboClass::Triangles, VboSubclass::BfcBackColors, GL_TRIANGLES);
+			drawVbos (VboClass::Quads, VboSubclass::BfcBackColors, GL_QUADS);
+			glDisable (GL_CULL_FACE);
+		}
+		else
+		{
+			VboSubclass colors;
+
+			if (m_config->randomColors())
+				colors = VboSubclass::RandomColors;
+			else
+				colors = VboSubclass::NormalColors;
+
+			drawVbos (VboClass::Triangles, colors, GL_TRIANGLES);
+			drawVbos (VboClass::Quads, colors, GL_QUADS);
+		}
+
+		drawVbos (VboClass::Lines, VboSubclass::NormalColors, GL_LINES);
+		glEnable (GL_LINE_STIPPLE);
+		drawVbos (VboClass::ConditionalLines, VboSubclass::NormalColors, GL_LINES);
+		glDisable (GL_LINE_STIPPLE);
+
+		if (m_config->drawAxes())
+		{
+			glDisableClientState (GL_NORMAL_ARRAY);
+			glBindBuffer (GL_ARRAY_BUFFER, m_axesVbo);
+			glVertexPointer (3, GL_FLOAT, 0, NULL);
+			glBindBuffer (GL_ARRAY_BUFFER, m_axesVbo);
+			glColorPointer (3, GL_FLOAT, 0, NULL);
+			glDrawArrays (GL_LINES, 0, 6);
+			glEnableClientState (GL_NORMAL_ARRAY);
+			CHECK_GL_ERROR();
+		}
+	}
+
+	glPopMatrix();
+	glBindBuffer (GL_ARRAY_BUFFER, 0);
+	glDisableClientState (GL_VERTEX_ARRAY);
+	glDisableClientState (GL_COLOR_ARRAY);
+	glDisableClientState (GL_NORMAL_ARRAY);
+	CHECK_GL_ERROR();
+	glDisable (GL_CULL_FACE);
+	glMatrixMode (GL_MODELVIEW);
+	glPolygonMode (GL_FRONT_AND_BACK, GL_FILL);
+}
+
+// =============================================================================
+//
+void GLRenderer::drawVbos (VboClass surface, VboSubclass colors, GLenum type)
+{
+	// Filter this through some configuration options
+	if ((isOneOf (surface, VboClass::Quads, VboClass::Triangles) and m_config->drawSurfaces() == false)
+		or (surface == VboClass::Lines and m_config->drawEdgeLines() == false)
+		or (surface == VboClass::ConditionalLines and m_config->drawConditionalLines() == false))
+	{
+		return;
+	}
+
+	int surfaceVboNumber = m_compiler->vboNumber(surface, VboSubclass::Surfaces);
+	int colorVboNumber = m_compiler->vboNumber(surface, colors);
+	int normalVboNumber = m_compiler->vboNumber(surface, VboSubclass::Normals);
+	m_compiler->prepareVBO(surfaceVboNumber, m_model);
+	m_compiler->prepareVBO(colorVboNumber, m_model);
+	m_compiler->prepareVBO(normalVboNumber, m_model);
+	GLuint surfaceVbo = m_compiler->vbo(surfaceVboNumber);
+	GLuint colorVbo = m_compiler->vbo(colorVboNumber);
+	GLuint normalVbo = m_compiler->vbo(normalVboNumber);
+	GLsizei count = m_compiler->vboSize(surfaceVboNumber) / 3;
+
+	if (count > 0)
+	{
+		glBindBuffer(GL_ARRAY_BUFFER, surfaceVbo);
+		glVertexPointer(3, GL_FLOAT, 0, nullptr);
+		CHECK_GL_ERROR();
+		glBindBuffer(GL_ARRAY_BUFFER, colorVbo);
+		glColorPointer(4, GL_FLOAT, 0, nullptr);
+		CHECK_GL_ERROR();
+		glBindBuffer(GL_ARRAY_BUFFER, normalVbo);
+		glNormalPointer(GL_FLOAT, 0, nullptr);
+		CHECK_GL_ERROR();
+		glDrawArrays(type, 0, count);
+		CHECK_GL_ERROR();
+	}
+}
+
+QPen GLRenderer::textPen() const
+{
+	return {m_useDarkBackground ? Qt::white : Qt::black};
+}
+
+bool GLRenderer::freeCameraAllowed() const
+{
+	return true;
+}
+
+void GLRenderer::paintEvent(QPaintEvent*)
+{
+	makeCurrent();
+	initGLData();
+	drawGLScene();
+
+	if (isDrawingSelectionScene())
+		return;
+
+	QPainter painter {this};
+	painter.setRenderHint(QPainter::Antialiasing);
+	overpaint(painter);
+}
+
+void GLRenderer::overpaint(QPainter &painter)
+{
+	// Draw a background for the selected camera
+	painter.setPen(thinBorderPen);
+	painter.setBrush(QBrush {QColor {0, 128, 160, 128}});
+	painter.drawRect(m_cameraIcons[static_cast<int>(camera())].hitRect);
+
+	// Draw the camera icons
+	for (const CameraIcon& info : m_cameraIcons)
+	{
+		// Don't draw the free camera icon when we can't use the free camera
+		if (info.camera == Camera::Free and not freeCameraAllowed())
+			continue;
+
+		painter.drawPixmap(info.targetRect, info.image, info.sourceRect);
+	}
+
+	// Draw a label for the current camera in the bottom left corner
+	{
+		QFontMetrics metrics {QFont {}};
+		int margin = 4;
+		painter.setPen(textPen());
+		painter.drawText(QPoint {margin, height() - margin - metrics.descent()}, currentCamera().name());
+	}
+}
+
+// =============================================================================
+//
+void GLRenderer::mouseReleaseEvent(QMouseEvent* event)
+{
+	bool wasLeft = (m_lastButtons & Qt::LeftButton) and not (event->buttons() & Qt::LeftButton);
+	m_panning = false;
+
+	// Check if we selected a camera icon
+	if (wasLeft and not mouseHasMoved())
+	{
+		for (CameraIcon& info : m_cameraIcons)
+		{
+			if (info.targetRect.contains (event->pos()))
+			{
+				setCamera (info.camera);
+				break;
+			}
+		}
+	}
+
+	update();
+	m_totalMouseMove = 0;
+}
+
+// =============================================================================
+//
+void GLRenderer::mousePressEvent(QMouseEvent* event)
+{
+	m_lastButtons = event->buttons();
+	m_totalMouseMove = 0;
+}
+
+// =============================================================================
+//
+void GLRenderer::mouseMoveEvent(QMouseEvent* event)
+{
+	int xMove = event->x() - m_mousePosition.x();
+	int yMove = event->y() - m_mousePosition.y();
+	m_totalMouseMove += qAbs(xMove) + qAbs(yMove);
+	m_isCameraMoving = false;
+
+	bool left = event->buttons() & Qt::LeftButton;
+	bool mid = event->buttons() & Qt::MidButton;
+	bool shift = event->modifiers() & Qt::ShiftModifier;
+
+	if (mid or (left and shift))
+	{
+		currentCamera().pan(xMove, yMove);
+		m_panning = true;
+		m_isCameraMoving = true;
+	}
+	else if (left and camera() == Camera::Free and (xMove != 0 or yMove != 0))
+	{
+		// Apply current rotation input to the rotation matrix
+		// ref: https://forums.ldraw.org/thread-22006-post-24426.html#pid24426
+		glPushMatrix();
+		glLoadIdentity();
+		// 0.6 is an arbitrary rotation sensitivity scalar
+		glRotatef(0.6 * hypot(xMove, yMove), yMove, xMove, 0);
+		glMultMatrixf(m_rotationMatrix.constData());
+		glGetFloatv(GL_MODELVIEW_MATRIX, m_rotationMatrix.data());
+		glPopMatrix();
+		m_isCameraMoving = true;
+	}
+
+	// Start the tool tip timer
+	m_toolTipTimer->start (500);
+
+	// Update 2d position
+	m_mousePosition = event->pos();
+	m_globalpos = event->globalPos();
+	m_mousePositionF = event->localPos();
+
+	highlightCursorObject();
+	update();
+	event->accept();
+}
+
+// =============================================================================
+//
+void GLRenderer::keyPressEvent(QKeyEvent* event)
+{
+	m_currentKeyboardModifiers = event->modifiers();
+}
+
+// =============================================================================
+//
+void GLRenderer::keyReleaseEvent(QKeyEvent* event)
+{
+	m_currentKeyboardModifiers = event->modifiers();
+	update();
+}
+
+// =============================================================================
+//
+void GLRenderer::wheelEvent(QWheelEvent* ev)
+{
+	makeCurrent();
+	currentCamera().zoomNotch(ev->delta() > 0);
+	m_isCameraMoving = true;
+	update();
+	ev->accept();
+}
+
+// =============================================================================
+//
+void GLRenderer::leaveEvent(QEvent*)
+{
+	m_toolTipTimer->stop();
+	update();
+}
+
+// =============================================================================
+//
+void GLRenderer::setCamera(Camera camera)
+{
+	// The edit mode may forbid the free camera.
+	if (freeCameraAllowed() or camera != Camera::Free)
+	{
+		m_camera = camera;
+		m_config->setCamera(static_cast<int>(camera));
+	}
+}
+
+/*
+ * Returns the set of objects found in the specified pixel area.
+ */
+QSet<LDObject*> GLRenderer::pick(const QRect& range)
+{
+	makeCurrent();
+	QSet<LDObject*> newSelection;
+
+	// Paint the picking scene
+	setPicking(true);
+	drawGLScene();
+
+	int x0 = range.left();
+	int y0 = range.top();
+	int x1 = x0 + range.width();
+	int y1 = y0 + range.height();
+
+	// Clamp the values to ensure they're within bounds
+	x0 = qMax (0, x0);
+	y0 = qMax (0, y0);
+	x1 = qMin (x1, width());
+	y1 = qMin (y1, height());
+	const int areawidth = (x1 - x0);
+	const int areaheight = (y1 - y0);
+	const qint32 numpixels = areawidth * areaheight;
+
+	// Allocate space for the pixel data.
+	QVector<unsigned char> pixelData;
+	pixelData.resize(4 * numpixels);
+
+	// Read pixels from the color buffer.
+	glReadPixels(x0, height() - y1, areawidth, areaheight, GL_RGBA, GL_UNSIGNED_BYTE, pixelData.data());
+
+	QSet<qint32> indices;
+
+	// Go through each pixel read and add them to the selection.
+	// Each pixel maps to an LDObject index injectively.
+	// Note: black is background, those indices are skipped.
+	for (unsigned char *pixelCursor = pixelData.begin(); pixelCursor < pixelData.end(); pixelCursor += 4)
+	{
+		qint32 index = pixelCursor[0] * 0x10000 + pixelCursor[1] * 0x100 + pixelCursor[2] * 0x1;
+		if (index != 0)
+			indices.insert(index);
+	}
+
+	// For each index read, resolve the LDObject behind it and add it to the selection.
+	for (qint32 index : indices)
+	{
+		LDObject* object = LDObject::fromID(index);
+
+		if (object != nullptr)
+			newSelection.insert(object);
+	}
+
+	setPicking(false);
+	repaint();
+	return newSelection;
+}
+
+/*
+ * Simpler version of GLRenderer::pick which simply picks whatever object on the cursor
+ */
+LDObject* GLRenderer::pick(int mouseX, int mouseY)
+{
+	unsigned char pixel[4];
+	makeCurrent();
+	setPicking(true);
+	drawGLScene();
+	glReadPixels(mouseX, height() - mouseY, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel);
+	LDObject* object = LDObject::fromID(pixel[0] * 0x10000 + pixel[1] * 0x100 + pixel[2]);
+	setPicking(false);
+	repaint();
+	return object;
+}
+
+// =============================================================================
+//
+void GLRenderer::setPicking(bool value)
+{
+	m_isDrawingSelectionScene = value;
+	setBackground();
+
+	if (m_isDrawingSelectionScene)
+	{
+		glDisable(GL_DITHER);
+
+		// Use particularly thick lines while picking ease up selecting lines.
+		glLineWidth(qMax<double>(m_config->lineThickness(), 6.5));
+	}
+	else
+	{
+		glEnable(GL_DITHER);
+
+		// Restore line thickness
+		glLineWidth(m_config->lineThickness());
+	}
+}
+
+// =============================================================================
+//
+void GLRenderer::forgetObject(LDObject* obj)
+{
+	if (m_objectAtCursor == obj)
+		m_objectAtCursor = nullptr;
+}
+
+// =============================================================================
+//
+QByteArray GLRenderer::capturePixels()
+{
+	QByteArray result;
+	result.resize (4 * width() * height());
+	m_takingScreenCapture = true;
+	update(); // Smile!
+	m_takingScreenCapture = false;
+	glReadPixels(0, 0, width(), height(), GL_RGBA, GL_UNSIGNED_BYTE, result.data());
+	return result;
+}
+
+/*
+ * Show a tooltip if the cursor is currently hovering over a camera icon.
+ */
+void GLRenderer::showCameraIconTooltip()
+{
+	for (CameraIcon & icon : m_cameraIcons)
+	{
+		if (icon.targetRect.contains (m_mousePosition))
+		{
+			QToolTip::showText(m_globalpos, m_cameras[static_cast<int>(icon.camera)].name());
+			update();
+			break;
+		}
+	}
+}
+
+// =============================================================================
+//
+void GLRenderer::zoomToFit()
+{
+	currentCamera().setZoom(30.0f);
+	bool lastfilled = false;
+	bool firstrun = true;
+	enum { black = 0xFF000000 };
+	bool inward = true;
+	int runaway = 50;
+
+	// 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 or zoom() < 0.0)
+		{
+			// Nothing to draw if we get here.
+			currentCamera().setZoom(30.0);
+			break;
+		}
+
+		currentCamera().zoomNotch(inward);
+		QVector<unsigned char> capture (4 * width() * height());
+		drawGLScene();
+		glReadPixels (0, 0, width(), height(), GL_RGBA, GL_UNSIGNED_BYTE, capture.data());
+		QImage image (capture.constData(), width(), height(), QImage::Format_ARGB32);
+		bool filled = false;
+
+		// Check the top and bottom rows
+		for (int i = 0; i < image.width(); ++i)
+		{
+			if (image.pixel (i, 0) != black or image.pixel (i, height() - 1) != black)
+			{
+				filled = true;
+				break;
+			}
+		}
+
+		// Left and right edges
+		if (filled == false)
+		{
+			for (int i = 0; i < image.height(); ++i)
+			{
+				if (image.pixel (0, i) != black or image.pixel (width() - 1, i) != black)
+				{
+					filled = true;
+					break;
+				}
+			}
+		}
+
+		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 = not 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 and not lastfilled)
+			{
+				currentCamera().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 (not filled and lastfilled)
+				break;
+
+			inward = not filled;
+		}
+
+		lastfilled = filled;
+	}
+
+	setPicking (false);
+}
+
+// =============================================================================
+//
+void GLRenderer::zoomAllToFit()
+{
+	zoomToFit();
+}
+
+// =============================================================================
+//
+void GLRenderer::highlightCursorObject()
+{
+	if (not m_config->highlightObjectBelowCursor() and objectAtCursor() == nullptr)
+		return;
+
+	LDObject* newObject = nullptr;
+	LDObject* oldObject = objectAtCursor();
+	qint32 newIndex;
+
+	if (m_isCameraMoving or not m_config->highlightObjectBelowCursor())
+	{
+		newIndex = 0;
+	}
+	else
+	{
+		setPicking (true);
+		drawGLScene();
+		setPicking (false);
+
+		unsigned char pixel[4];
+		glReadPixels (m_mousePosition.x(), height() - m_mousePosition.y(), 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &pixel[0]);
+		newIndex = pixel[0] * 0x10000 | pixel[1] * 0x100 | pixel[2];
+	}
+
+	if (newIndex != (oldObject ? oldObject->id() : 0))
+	{
+		if (newIndex != 0)
+			newObject = LDObject::fromID (newIndex);
+
+		m_objectAtCursor = newObject;
+
+		if (oldObject)
+			emit objectHighlightingChanged(oldObject);
+
+		if (newObject)
+			emit objectHighlightingChanged(newObject);
+	}
+
+	update();
+}
+
+bool GLRenderer::mouseHasMoved() const
+{
+	return m_totalMouseMove >= 10;
+}
+
+QPoint const& GLRenderer::mousePosition() const
+{
+	return m_mousePosition;
+}
+
+QPointF const& GLRenderer::mousePositionF() const
+{
+	return m_mousePositionF;
+}
+
+Qt::KeyboardModifiers GLRenderer::keyboardModifiers() const
+{
+	return m_currentKeyboardModifiers;
+}
+
+Camera GLRenderer::camera() const
+{
+	return m_camera;
+}
+
+double GLRenderer::panning (Axis ax) const
+{
+	return (ax == X) ? currentCamera().panningX() : currentCamera().panningY();
+}
+
+double GLRenderer::zoom()
+{
+	return currentCamera().zoom();
+}
+
+const QGenericMatrix<4, 4, GLfloat>& GLRenderer::rotationMatrix() const
+{
+	return m_rotationMatrix;
+}
+
+bool GLRenderer::isDrawingSelectionScene() const
+{
+	return m_isDrawingSelectionScene;
+}
+
+Qt::MouseButtons GLRenderer::lastButtons() const
+{
+	return m_lastButtons;
+}
+
+const Model* GLRenderer::model() const
+{
+	return m_model;
+}

mercurial