src/glRenderer.cc

Tue, 02 Sep 2014 13:34:01 +0300

author
Teemu Piippo <crimsondusk64@gmail.com>
date
Tue, 02 Sep 2014 13:34:01 +0300
changeset 874
ba75c789667e
parent 869
7184f3bae695
child 875
ce8e9b37d44f
permissions
-rw-r--r--

- added line loop mode (doesn't actually create anything yet)

/*
 *  LDForge: LDraw parts authoring CAD
 *  Copyright (C) 2013, 2014 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 <QGLWidget>
#include <QWheelEvent>
#include <QMouseEvent>
#include <QContextMenuEvent>
#include <QInputDialog>
#include <QToolTip>
#include <QTextDocument>
#include <QTimer>
#include <GL/glu.h>
#include "main.h"
#include "configuration.h"
#include "ldDocument.h"
#include "glRenderer.h"
#include "colors.h"
#include "mainWindow.h"
#include "miscallenous.h"
#include "editHistory.h"
#include "dialogs.h"
#include "addObjectDialog.h"
#include "messageLog.h"
#include "glCompiler.h"
#include "primitives.h"

const LDFixedCamera g_FixedCameras[6] =
{
	{{  1,  0, 0 }, X, Z, false, false, false }, // top
	{{  0,  0, 0 }, X, Y, false,  true, false }, // front
	{{  0,  1, 0 }, Z, Y,  true,  true, false }, // left
	{{ -1,  0, 0 }, X, Z, false,  true, true }, // bottom
	{{  0,  0, 0 }, X, Y,  true,  true, true }, // back
	{{  0, -1, 0 }, Z, Y, false,  true, true }, // right
};

CFGENTRY (String, BackgroundColor,			"#FFFFFF")
CFGENTRY (String, MainColor,					"#A0A0A0")
CFGENTRY (Float, MainColorAlpha,				1.0)
CFGENTRY (Int, LineThickness,				2)
CFGENTRY (Bool, BFCRedGreenView,			false)
CFGENTRY (Int, Camera,						EFreeCamera)
CFGENTRY (Bool, BlackEdges,					false)
CFGENTRY (Bool, DrawAxes,					false)
CFGENTRY (Bool, DrawWireframe,				false)
CFGENTRY (Bool, UseLogoStuds,				false)
CFGENTRY (Bool, AntiAliasedLines,			true)
CFGENTRY (Bool, RandomColors,				false)
CFGENTRY (Bool, HighlightObjectBelowCursor,	true)
CFGENTRY (Bool, DrawSurfaces,				true)
CFGENTRY (Bool, DrawEdgeLines,				true)
CFGENTRY (Bool, DrawConditionalLines,		true)

// 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")
};

struct LDGLAxis
{
	const QColor col;
	const Vertex vert;
};

// Definitions for visual axes, drawn on the screen
static const LDGLAxis g_GLAxes[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
};

static bool RendererInitialized (false);

// =============================================================================
//
GLRenderer::GLRenderer (QWidget* parent) : QGLWidget (parent)
{
	m_isPicking = false;
	m_camera = (ECamera) cfg::Camera;
	m_drawToolTip = false;
	m_editmode = AbstractEditMode::createByType (this, EditModeType::Select);
	m_panning = false;
	m_compiler = new GLCompiler (this);
	setDrawOnly (false);
	setMessageLog (null);
	m_width = m_height = -1;
	m_position3D = Origin;
	m_toolTipTimer = new QTimer (this);
	m_toolTipTimer->setSingleShot (true);
	m_isCameraMoving = false;
	m_thinBorderPen = QPen (QColor (0, 0, 0, 208), 1, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin);
	m_thinBorderPen.setWidth (1);
	setAcceptDrops (true);
	connect (m_toolTipTimer, SIGNAL (timeout()), this, SLOT (slot_toolTipTimer()));

	// Init camera icons
	for (ECamera cam = EFirstCamera; cam < ENumCameras; ++cam)
	{
		QString iconname = format ("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;

	if (messageLog())
		messageLog()->setRenderer (null);

	m_compiler->setRenderer (null);
	delete m_compiler;
	delete m_editmode;

	glDeleteBuffers (1, &m_axesVBO);
	glDeleteBuffers (1, &m_axesColorVBO);
}

// =============================================================================
// 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 (cfg::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);
	}
}

// =============================================================================
//
void GLRenderer::needZoomToFit()
{
	if (document() != null)
		currentDocumentData().needZoomToFit = true;
}

// =============================================================================
//
void GLRenderer::resetAngles()
{
	rot (X) = 30.0f;
	rot (Y) = 325.f;
	pan (X) = pan (Y) = rot (Z) = 0.0f;
	needZoomToFit();
}

// =============================================================================
//
void GLRenderer::resetAllAngles()
{
	ECamera oldcam = camera();

	for (int i = 0; i < 7; ++i)
	{
		setCamera ((ECamera) i);
		resetAngles();
	}

	setCamera (oldcam);
}

// =============================================================================
//
void GLRenderer::initializeGL()
{
#ifdef USE_QT5
	initializeOpenGLFunctions();
#endif
	setBackground();
	glLineWidth (cfg::LineThickness);
	glLineStipple (1, 0x6666);
	setAutoFillBackground (false);
	setMouseTracking (true);
	setFocusPolicy (Qt::WheelFocus);
	compiler()->initialize();
	initializeAxes();
	RendererInitialized = true;
}

// =============================================================================
//
void GLRenderer::initializeAxes()
{
	float axesdata[18];
	float colordata[18];
	memset (axesdata, 0, sizeof axesdata);

	for (int i = 0; i < 3; ++i)
	{
		for_axes (ax)
		{
			axesdata[(i * 6) + ax] = g_GLAxes[i].vert[ax];
			axesdata[(i * 6) + 3 + ax] = -g_GLAxes[i].vert[ax];
		}

		for (int j = 0; j < 2; ++j)
		{
			colordata[(i * 6) + (j * 3) + 0] = g_GLAxes[i].col.red();
			colordata[(i * 6) + (j * 3) + 1] = g_GLAxes[i].col.green();
			colordata[(i * 6) + (j * 3) + 2] = g_GLAxes[i].col.blue();
		}
	}

	glGenBuffers (1, &m_axesVBO);
	glBindBuffer (GL_ARRAY_BUFFER, m_axesVBO);
	glBufferData (GL_ARRAY_BUFFER, sizeof axesdata, axesdata, 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);
}

// =============================================================================
//
QColor GLRenderer::getMainColor()
{
	QColor col (cfg::MainColor);

	if (not col.isValid())
		return QColor (0, 0, 0);

	col.setAlpha (cfg::MainColorAlpha * 255.f);
	return col;
}

// =============================================================================
//
void GLRenderer::setBackground()
{
	if (isPicking())
	{
		glClearColor (0.0f, 0.0f, 0.0f, 1.0f);
		return;
	}

	QColor col (cfg::BackgroundColor);

	if (not col.isValid())
		return;

	col.setAlpha (255);

	m_darkbg = Luma (col) < 80;
	m_bgcolor = col;
	qglClearColor (col);
}

// =============================================================================
//
void GLRenderer::refresh()
{
	update();

	if (isVisible())
		swapBuffers();
}

// =============================================================================
//
void GLRenderer::hardRefresh()
{
	if (not RendererInitialized)
		return;

	compiler()->compileDocument (CurrentDocument());
	refresh();
}

// =============================================================================
//
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 (document() == null)
		return;

	if (currentDocumentData().needZoomToFit)
	{
		currentDocumentData().needZoomToFit = false;
		zoomAllToFit();
	}

	if (cfg::DrawWireframe and not isPicking())
		glPolygonMode (GL_FRONT_AND_BACK, GL_LINE);

	glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glEnable (GL_DEPTH_TEST);

	if (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 (camera() != EFrontCamera and 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 (camera() == 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);
	}

	glEnableClientState (GL_VERTEX_ARRAY);
	glEnableClientState (GL_COLOR_ARRAY);

	if (isPicking())
	{
		drawVBOs (VBOSF_Triangles, VBOCM_PickColors, GL_TRIANGLES);
		drawVBOs (VBOSF_Quads, VBOCM_PickColors, GL_QUADS);
		drawVBOs (VBOSF_Lines, VBOCM_PickColors, GL_LINES);
		drawVBOs (VBOSF_CondLines, VBOCM_PickColors, GL_LINES);
	}
	else
	{
		if (cfg::BFCRedGreenView)
		{
			glEnable (GL_CULL_FACE);
			glCullFace (GL_BACK);
			drawVBOs (VBOSF_Triangles, VBOCM_BFCFrontColors, GL_TRIANGLES);
			drawVBOs (VBOSF_Quads, VBOCM_BFCFrontColors, GL_QUADS);
			glCullFace (GL_FRONT);
			drawVBOs (VBOSF_Triangles, VBOCM_BFCBackColors, GL_TRIANGLES);
			drawVBOs (VBOSF_Quads, VBOCM_BFCBackColors, GL_QUADS);
			glDisable (GL_CULL_FACE);
		}
		else
		{
			if (cfg::RandomColors)
			{
				drawVBOs (VBOSF_Triangles, VBOCM_RandomColors, GL_TRIANGLES);
				drawVBOs (VBOSF_Quads, VBOCM_RandomColors, GL_QUADS);
			}
			else
			{
				drawVBOs (VBOSF_Triangles, VBOCM_NormalColors, GL_TRIANGLES);
				drawVBOs (VBOSF_Quads, VBOCM_NormalColors, GL_QUADS);
			}
		}

		drawVBOs (VBOSF_Lines, VBOCM_NormalColors, GL_LINES);
		glEnable (GL_LINE_STIPPLE);
		drawVBOs (VBOSF_CondLines, VBOCM_NormalColors, GL_LINES);
		glDisable (GL_LINE_STIPPLE);

		if (cfg::DrawAxes)
		{
			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);
			CHECK_GL_ERROR();
		}
	}

	glPopMatrix();
	glBindBuffer (GL_ARRAY_BUFFER, 0);
	glDisableClientState (GL_VERTEX_ARRAY);
	glDisableClientState (GL_COLOR_ARRAY);
	CHECK_GL_ERROR();
	glDisable (GL_CULL_FACE);
	glMatrixMode (GL_MODELVIEW);
	glPolygonMode (GL_FRONT_AND_BACK, GL_FILL);
}

// =============================================================================
//
void GLRenderer::drawVBOs (EVBOSurface surface, EVBOComplement colors, GLenum type)
{
	// Filter this through some configuration options
	if ((Eq (surface, VBOSF_Quads, VBOSF_Triangles) and cfg::DrawSurfaces == false) or
		(surface == VBOSF_Lines and cfg::DrawEdgeLines == false) or
		(surface == VBOSF_CondLines and cfg::DrawConditionalLines == false))
	{
		return;
	}

	int surfacenum = m_compiler->vboNumber (surface, VBOCM_Surfaces);
	int colornum = m_compiler->vboNumber (surface, colors);
	m_compiler->prepareVBO (surfacenum);
	m_compiler->prepareVBO (colornum);
	GLuint surfacevbo = m_compiler->vbo (surfacenum);
	GLuint colorvbo = m_compiler->vbo (colornum);
	GLsizei count = m_compiler->vboSize (surfacenum) / 3;

	if (count > 0)
	{
		glBindBuffer (GL_ARRAY_BUFFER, surfacevbo);
		glVertexPointer (3, GL_FLOAT, 0, null);
		CHECK_GL_ERROR();
		glBindBuffer (GL_ARRAY_BUFFER, colorvbo);
		glColorPointer (4, GL_FLOAT, 0, null);
		CHECK_GL_ERROR();
		glDrawArrays (type, 0, count);
		CHECK_GL_ERROR();
	}
}

// =============================================================================
// 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 LDFixedCamera* cam = &g_FixedCameras[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::Coordinate);
		cy = Grid::Snap (cy, Grid::Coordinate);
	}

	cx *= negXFac;
	cy *= negYFac;

	RoundToDecimals (cx, 4);
	RoundToDecimals (cy, 4);

	// Create the vertex from the coordinates
	pos3d.setCoordinate (axisX, cx);
	pos3d.setCoordinate (axisY, cy);
	pos3d.setCoordinate ((Axis) (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)
{
	GLfloat m[16];
	const LDFixedCamera* cam = &g_FixedCameras[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.setX ((m[0] * x) + (m[1] * y) + (m[2] * z) + m[3]);
	transformed.setY ((m[4] * x) + (m[5] * y) + (m[6] * z) + m[7]);
	transformed.setZ ((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);
}

QPen GLRenderer::textPen() const
{
	return QPen (m_darkbg ? Qt::white : Qt::black);
}

QPen GLRenderer::linePen() const
{
	QPen linepen (m_thinBorderPen);
	linepen.setWidth (2);
	linepen.setColor (Luma (m_bgcolor) < 40 ? Qt::white : Qt::black);
	return linepen;
}

// =============================================================================
//
void GLRenderer::paintEvent (QPaintEvent*)
{
	doMakeCurrent();
	m_virtWidth = zoom();
	m_virtHeight = (m_height * m_virtWidth) / m_width;
	initGLData();
	drawGLScene();

	QPainter paint (this);
	QFontMetrics metrics = QFontMetrics (QFont());
	paint.setRenderHint (QPainter::HighQualityAntialiasing);

	// If we wish to only draw the brick, stop here
	if (isDrawOnly())
		return;

#ifndef RELEASE
	if (not isPicking())
	{
		QString text = format ("Rotation: (%1, %2, %3)\nPanning: (%4, %5), Zoom: %6",
			rot(X), rot(Y), rot(Z), pan(X), pan(Y), zoom());
		QRect textSize = metrics.boundingRect (0, 0, m_width, m_height, Qt::AlignCenter, text);
		paint.setPen (textPen());
		paint.drawText ((width() - textSize.width()) / 2, height() - textSize.height(), textSize.width(),
			textSize.height(), Qt::AlignCenter, text);
	}
#endif

	if (camera() != EFreeCamera and not isPicking())
	{
		// Paint the overlay image if we have one
		const LDGLOverlay& overlay = currentDocumentData().overlays[camera()];

		if (overlay.img != null)
		{
			QPoint v0 = coordconv3_2 (currentDocumentData().overlays[camera()].v0),
					   v1 = coordconv3_2 (currentDocumentData().overlays[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 = format (tr ("X: %1, Y: %2, Z: %3"), m_position3D[X], m_position3D[Y], m_position3D[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);
	}

	if (not isPicking())
	{
		// Draw edit mode HUD
		m_editmode->render (paint);

		// 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 camera icons
		for (CameraIcon& info : m_cameraIcons)
		{
			// Don't draw the free camera icon when in draw mode
			if (&info == &m_cameraIcons[EFreeCamera] and not m_editmode->allowFreeCamera())
				continue;

			paint.drawPixmap (info.destRect, *info.img, info.srcRect);
		}

		QString formatstr = tr ("%1 Camera");

		// Draw a label for the current camera in the bottom left corner
		{
			const int margin = 4;

			QString label;
			label = format (formatstr, tr (g_CameraNames[camera()]));
			paint.setPen (textPen());
			paint.drawText (QPoint (margin, height() - (margin + metrics.descent())), label);
		}

		// Tool tips
		if (m_drawToolTip)
		{
			if (not m_cameraIcons[m_toolTipCamera].destRect.contains (m_mousePosition))
				m_drawToolTip = false;
			else
			{
				QString label = format (formatstr, tr (g_CameraNames[m_toolTipCamera]));
				QToolTip::showText (m_globalpos, label);
			}
		}
	}

	// Message log
	if (messageLog())
	{
		int y = 0;
		const int margin = 2;
		QColor penColor = textPen().color();

		for (const MessageManager::Line& line : messageLog()->getLines())
		{
			penColor.setAlphaF (line.alpha);
			paint.setPen (penColor);
			paint.drawText (QPoint (margin, y + margin + metrics.ascent()), line.text);
			y += metrics.height();
		}
	}
}

// =============================================================================
//
void GLRenderer::drawBlip (QPainter& paint, QPointF 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::clampAngle (double& angle) const
{
	while (angle < 0)
		angle += 360.0;

	while (angle > 360.0)
		angle -= 360.0;
}

// =============================================================================
//
void GLRenderer::mouseReleaseEvent (QMouseEvent* ev)
{
	const bool wasLeft = (m_lastButtons & Qt::LeftButton) and not (ev->buttons() & Qt::LeftButton);

	Qt::MouseButtons releasedbuttons = m_lastButtons & ~ev->buttons();

	if (m_panning)
		m_panning = false;

	if (wasLeft)
	{
		// Check if we selected a camera icon
		if (not mouseHasMoved())
		{
			for (CameraIcon & info : m_cameraIcons)
			{
				if (info.destRect.contains (ev->pos()))
				{
					setCamera (info.cam);
					goto end;
				}
			}
		}
	}

	if (not isDrawOnly())
	{
		AbstractEditMode::MouseEventData data;
		data.ev = ev;
		data.mouseMoved = mouseHasMoved();
		data.keymods = m_keymods;
		data.releasedButtons = releasedbuttons;

		if (m_editmode->mouseReleased (data))
			goto end;
	}

end:
	update();
	m_totalmove = 0;
}

// =============================================================================
//
void GLRenderer::mousePressEvent (QMouseEvent* ev)
{
	m_totalmove = 0;
	m_lastButtons = ev->buttons();

	if (m_editmode->mousePressed (ev))
		ev->accept();
}

// =============================================================================
//
void GLRenderer::mouseMoveEvent (QMouseEvent* ev)
{
	int dx = ev->x() - m_mousePosition.x();
	int dy = ev->y() - m_mousePosition.y();
	m_totalmove += Abs (dx) + Abs (dy);
	setCameraMoving (false);

	if (not m_editmode->mouseMoved (ev))
	{
		const bool left = ev->buttons() & Qt::LeftButton,
				mid = ev->buttons() & Qt::MidButton,
				shift = ev->modifiers() & Qt::ShiftModifier;

		if (mid or (left and shift))
		{
			pan (X) += 0.03f * dx * (zoom() / 7.5f);
			pan (Y) -= 0.03f * dy * (zoom() / 7.5f);
			m_panning = true;
			setCameraMoving (true);
		}
		elif (left and camera() == EFreeCamera)
		{
			rot (X) = rot (X) + dy;
			rot (Y) = rot (Y) + dx;

			clampAngle (rot (X));
			clampAngle (rot (Y));
			setCameraMoving (true);
		}
	}

	// Start the tool tip timer
	if (not m_drawToolTip)
		m_toolTipTimer->start (500);

	// Update 2d position
	m_mousePosition = ev->pos();
	m_mousePositionF = ev->posF();
	m_globalpos = ev->globalPos();

	// Calculate 3d position of the cursor
	m_position3D = (camera() != EFreeCamera) ? coordconv2_3 (m_mousePosition, true) : Origin;

	highlightCursorObject();
	update();
	ev->accept();
}

// =============================================================================
//
void GLRenderer::keyPressEvent (QKeyEvent* ev)
{
	m_keymods = ev->modifiers();
}

// =============================================================================
//
void GLRenderer::keyReleaseEvent (QKeyEvent* ev)
{
	m_keymods = ev->modifiers();
}

// =============================================================================
//
void GLRenderer::wheelEvent (QWheelEvent* ev)
{
	doMakeCurrent();

	zoomNotch (ev->delta() > 0);
	zoom() = Clamp (zoom(), 0.01, 10000.0);
	setCameraMoving (true);
	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 ECamera cam)
{
	// The edit mode may forbid the free camera.
	if (cam == EFreeCamera and not m_editmode->allowFreeCamera())
		return;

	m_camera = cam;
	cfg::Camera = (int) cam;
	g_win->updateEditModeActions();
}

// =============================================================================
//
void GLRenderer::pick (int mouseX, int mouseY, bool additive)
{
	pick (QRect (mouseX, mouseY, mouseX + 1, mouseY + 1), additive);
}

// =============================================================================
//
void GLRenderer::pick (QRect const& range, bool additive)
{
	doMakeCurrent();

	// Clear the selection if we do not wish to add to it.
	if (not additive)
	{
		LDObjectList oldsel = Selection();
		CurrentDocument()->clearSelection();

		for (LDObjectPtr obj : oldsel)
			compileObject (obj);
	}

	// 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 = 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);

	LDObjectPtr removedObj;
	QList<qint32> indices;

	// Go through each pixel read and add them to the selection.
	// Note: black is background, those indices are skipped.
	for (qint32 i = 0; i < numpixels; ++i)
	{
		qint32 idx =
			(*(pixelptr + 0) * 0x10000) +
			(*(pixelptr + 1) * 0x100) +
			*(pixelptr + 2);
		pixelptr += 4;

		if (idx != 0)
			indices << idx;
	}

	RemoveDuplicates (indices);

	for (qint32 idx : indices)
	{
		LDObjectPtr 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 (additive)
		{
			if (obj->isSelected())
			{
				obj->deselect();
				removedObj = obj;
				break;
			}
		}

		obj->select();
	}

	delete[] pixeldata;

	// Update everything now.
	g_win->updateSelection();

	// Recompile the objects now to update their color
	for (LDObjectPtr obj : Selection())
		compileObject (obj);

	if (removedObj)
		compileObject (removedObj);

	setPicking (false);
	repaint();
}

//
// Simpler version of GLRenderer::pick which simply picks whatever object on the screen
//
LDObjectPtr GLRenderer::pickOneObject (int mouseX, int mouseY)
{
	uchar pixel[4];
	doMakeCurrent();
	setPicking (true);
	drawGLScene();
	glReadPixels (mouseX, m_height - mouseY, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel);
	LDObjectPtr obj = LDObject::fromID ((pixel[0] * 0x10000) + (pixel[1] * 0x100) + pixel[2]);
	setPicking (false);
	repaint();
	return obj;
}

// =============================================================================
//
void GLRenderer::setEditMode (EditModeType a)
{
	if (m_editmode != null and m_editmode->type() == a)
		return;

	delete m_editmode;
	m_editmode = AbstractEditMode::createByType (this, a);

	// If we cannot use the free camera, use the top one instead.
	if (camera() == EFreeCamera and not m_editmode->allowFreeCamera())
		setCamera (ETopCamera);

	g_win->updateEditModeActions();
	update();
}

// =============================================================================
//
EditModeType GLRenderer::currentEditModeType() const
{
	return m_editmode->type();
}

// =============================================================================
//
void GLRenderer::setDocument (LDDocumentPtr const& a)
{
	m_document = a;

	if (a != null)
	{
		initOverlaysFromObjects();

		if (not currentDocumentData().init)
		{
			resetAllAngles();
			currentDocumentData().init = true;
		}

		currentDocumentData().needZoomToFit = true;
	}
}

// =============================================================================
//
void GLRenderer::setPicking (const bool& a)
{
	m_isPicking = a;
	setBackground();

	if (isPicking())
	{
		glDisable (GL_DITHER);

		// Use particularly thick lines while picking ease up selecting lines.
		glLineWidth (Max<double> (cfg::LineThickness, 6.5));
	}
	else
	{
		glEnable (GL_DITHER);

		// Restore line thickness
		glLineWidth (cfg::LineThickness);
	}
}

// =============================================================================
//
void GLRenderer::getRelativeAxes (Axis& relX, Axis& relY) const
{
	const LDFixedCamera* cam = &g_FixedCameras[camera()];
	relX = cam->axisX;
	relY = cam->axisY;
}

// =============================================================================
//
Axis GLRenderer::getRelativeZ() const
{
	const LDFixedCamera* cam = &g_FixedCameras[camera()];
	return (Axis) (3 - cam->axisX - cam->axisY);
}

// =============================================================================
//
static QList<Vertex> GetVerticesOf (LDObjectPtr obj)
{
	QList<Vertex> verts;

	if (obj->numVertices() >= 2)
	{
		for (int i = 0; i < obj->numVertices(); ++i)
			verts << obj->vertex (i);
	}
	elif (obj->type() == OBJ_Subfile)
	{
		LDSubfilePtr ref = obj.staticCast<LDSubfile>();
		LDObjectList objs = ref->inlineContents (true, false);

		for (LDObjectPtr obj : objs)
		{
			verts << GetVerticesOf (obj);
			obj->destroy();
		}
	}

	return verts;
}

// =============================================================================
//
void GLRenderer::compileObject (LDObjectPtr obj)
{
	compiler()->stageForCompilation (obj);
}

// =============================================================================
//
void GLRenderer::forgetObject (LDObjectPtr obj)
{
	if (compiler() != null)
		compiler()->dropObject (obj);
}

// =============================================================================
//
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_mousePosition))
		{
			m_toolTipCamera = icon.cam;
			m_drawToolTip = true;
			update();
			break;
		}
	}
}

// =============================================================================
//
Axis GLRenderer::getCameraAxis (bool y, ECamera camid)
{
	if (camid == (ECamera) -1)
		camid = camera();

	const LDFixedCamera* cam = &g_FixedCameras[camid];
	return (y) ? cam->axisY : cam->axisX;
}

// =============================================================================
//
bool GLRenderer::setupOverlay (ECamera 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())
	{
		CriticalError (tr ("Failed to load overlay image!"));
		currentDocumentData().overlays[cam].invalid = true;
		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;
	info.invalid = false;

	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 = Origin;
	info.v0.setCoordinate (x2d, -(info.ox * info.lw * negXFac) / img->width());
	info.v0.setCoordinate (y2d, (info.oy * info.lh * negYFac) / img->height());
	info.v1.setCoordinate (x2d, info.v0[x2d] + info.lw);
	info.v1.setCoordinate (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()
{
	zoom() = 30.0f;

	if (document() == null or m_width == -1 or m_height == -1)
		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 or 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 or imgdata[((h - 1) * w) + i] != white)
			{
				filled = true;
				break;
			}
		}

		// Left and right edges
		if (filled == false)
		{
			for (int i = 0; i < h; ++i)
			{
				if (imgdata[i * w] != white or imgdata[(i * w) + w - 1] != white)
				{
					filled = true;
					break;
				}
			}
		}

		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 = 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)
			{
				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;
	}

	setBackground();
	setPicking (false);
}

// =============================================================================
//
void GLRenderer::zoomAllToFit()
{
	ECamera oldcam = camera();

	for (ECamera cam = EFirstCamera; cam < ENumCameras; ++cam)
	{
		setCamera (cam);
		zoomToFit();
	}

	setCamera (oldcam);
}

// =============================================================================
//
void GLRenderer::mouseDoubleClickEvent (QMouseEvent* ev)
{
	if (m_editmode->mouseDoubleClicked (ev))
		ev->accept();
}

// =============================================================================
//
LDOverlayPtr GLRenderer::findOverlayObject (ECamera cam)
{
	for (LDObjectPtr obj : document()->objects())
	{
		LDOverlayPtr ovlobj = obj.dynamicCast<LDOverlay>();

		if (ovlobj != null and obj.staticCast<LDOverlay>()->camera() == cam)
			return ovlobj;
	}

	return LDOverlayPtr();
}

// =============================================================================
//
// Read in overlays from the current file and update overlay info accordingly.
//
void GLRenderer::initOverlaysFromObjects()
{
	for (ECamera cam = EFirstCamera; cam < ENumCameras; ++cam)
	{
		if (cam == EFreeCamera)
			continue;

		LDGLOverlay& meta = currentDocumentData().overlays[cam];
		LDOverlayPtr ovlobj = findOverlayObject (cam);

		if (ovlobj == null and meta.img != null)
		{
			delete meta.img;
			meta.img = null;
		}
		elif (ovlobj != null and
			(meta.img == null or meta.fname != ovlobj->fileName()) and
			not meta.invalid)
		{
			setupOverlay (cam, ovlobj->fileName(), ovlobj->x(),
				ovlobj->y(), ovlobj->width(), ovlobj->height());
		}
	}
}

// =============================================================================
//
void GLRenderer::updateOverlayObjects()
{
	for (ECamera cam = EFirstCamera; cam < ENumCameras; ++cam)
	{
		if (cam == EFreeCamera)
			continue;

		LDGLOverlay& meta = currentDocumentData().overlays[cam];
		LDOverlayPtr ovlobj = findOverlayObject (cam);

		if (meta.img == null and ovlobj != null)
		{
			// If this is the last overlay image, we need to remove the empty space after it as well.
			LDObjectPtr nextobj = ovlobj->next();

			if (nextobj and nextobj->type() == OBJ_Empty)
				nextobj->destroy();

			// If the overlay object was there and the overlay itself is
			// not, remove the object.
			ovlobj->destroy();
		}
		elif (meta.img != null and ovlobj == null)
		{
			// Inverse case: image is there but the overlay object is
			// not, thus create the object.
			ovlobj = LDSpawn<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 < document()->getObjectCount(); ++i)
			{
				LDObjectPtr obj = document()->getObject (i);

				if (obj->isScemantic())
				{
					found = true;
					break;
				}

				if (obj->type() == OBJ_Overlay)
					lastOverlay = i;
			}

			if (lastOverlay != -1)
				document()->insertObj (lastOverlay + 1, ovlobj);
			else
			{
				document()->insertObj (i, ovlobj);

				if (found)
					document()->insertObj (i + 1, LDSpawn<LDEmpty>());
			}
		}

		if (meta.img != null and ovlobj != null)
		{
			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();
}

// =============================================================================
//
void GLRenderer::highlightCursorObject()
{
	if (not cfg::HighlightObjectBelowCursor and objectAtCursor() == null)
		return;

	LDObjectWeakPtr newObject;
	LDObjectWeakPtr oldObject = objectAtCursor();
	qint32 newIndex;

	if (isCameraMoving() or not cfg::HighlightObjectBelowCursor)
	{
		newIndex = 0;
	}
	else
	{
		setPicking (true);
		drawGLScene();
		setPicking (false);

		unsigned char pixel[4];
		glReadPixels (m_mousePosition.x(), m_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 != null ? oldObject.toStrongRef()->id() : 0))
	{
		if (newIndex != 0)
			newObject = LDObject::fromID (newIndex);

		setObjectAtCursor (newObject);

		if (oldObject != null)
			compileObject (oldObject);

		if (newObject != null)
			compileObject (newObject);
	}

	update();
}

void GLRenderer::dragEnterEvent (QDragEnterEvent* ev)
{
	if (g_win != null and ev->source() == g_win->getPrimitivesTree() and g_win->getPrimitivesTree()->currentItem() != null)
		ev->acceptProposedAction();
}

void GLRenderer::dropEvent (QDropEvent* ev)
{
	if (g_win != null and ev->source() == g_win->getPrimitivesTree())
	{
		QString primName = static_cast<SubfileListItem*> (g_win->getPrimitivesTree()->currentItem())->primitive()->name;
		LDSubfilePtr ref = LDSpawn<LDSubfile>();
		ref->setColor (MainColor());
		ref->setFileInfo (GetDocument (primName));
		ref->setPosition (Origin);
		ref->setTransform (IdentityMatrix);
		LDDocument::current()->insertObj (g_win->getInsertionPoint(), ref);
		ref->select();
		g_win->buildObjList();
		g_win->R()->refresh();
		ev->acceptProposedAction();
	}
}

Vertex const& GLRenderer::position3D() const
{
	return m_position3D;
}

LDFixedCamera const& GLRenderer::getFixedCamera (ECamera cam) const
{
	return g_FixedCameras[cam];
}

bool GLRenderer::mouseHasMoved() const
{
	return m_totalmove >= 10;
}

QPoint const& GLRenderer::mousePosition() const
{
	return m_mousePosition;
}

QPointF const& GLRenderer::mousePositionF() const
{
	return m_mousePositionF;
}

void GLRenderer::doMakeCurrent()
{
	makeCurrent();
	initializeOpenGLFunctions();
}

int GLRenderer::depthNegateFactor() const
{
	return g_FixedCameras[camera()].negatedDepth ? -1 : 1;
}

LDFixedCamera const& GetFixedCamera (ECamera cam)
{
	assert (cam != EFreeCamera);
	return g_FixedCameras[cam];
}

mercurial