src/ui/canvas.cpp

Mon, 09 Mar 2020 22:12:50 +0200

author
Teemu Piippo <teemu@hecknology.net>
date
Mon, 09 Mar 2020 22:12:50 +0200
changeset 76
7c4a63a02632
parent 73
97df974b5ed5
child 79
5fe2dd4e161a
permissions
-rw-r--r--

finished splitQuadrilateral theoretically (untested)

#include <QMouseEvent>
#include <QPainter>
#include "canvas.h"

Canvas::Canvas(
	Model* model,
	DocumentManager* documents,
	const ldraw::ColorTable& colorTable,
	QWidget* parent) :
	PartRenderer{model, documents, colorTable, parent}
{
	this->setMouseTracking(true);
}

void Canvas::handleSelectionChange(const QSet<ldraw::id_t>& selectedIds, const QSet<ldraw::id_t>& deselectedIds)
{
	Q_ASSERT(not selectedIds.contains(ldraw::NULL_ID));
	this->selection.subtract(deselectedIds);
	this->selection.unite(selectedIds);
	this->compiler->setSelectedObjects(this->selection);
	this->update();
}

void Canvas::mouseMoveEvent(QMouseEvent* event)
{
	const ldraw::id_t id = this->pick(event->pos());
	this->highlighted = id;
	this->totalMouseMove += (event->pos() - this->lastMousePosition).manhattanLength();
	this->worldPosition = this->screenToModelCoordinates(event->pos(), this->gridPlane);
	if (this->worldPosition.has_value())
	{
		/*
		 * Snap the position to grid. This procedure is basically the "change of basis" and almost follows the
		 * Aโปยน ร— M ร— A formula which is used to perform a transformation in some other coordinate system, except
		 * we actually use the inverted matrix first and the regular one last to perform the transformation of
		 * grid coordinates in our XY coordinate system. Also, we're rounding the coordinates which is obviously
		 * not a linear transformation, but fits the pattern anyway.
		 */
		// First transform the coordinates to the XY plane...
		this->worldPosition = glm::inverse(this->gridMatrix) * glm::vec4{*this->worldPosition, 1};
		// Then round the coordinates to integer precision...
		this->worldPosition = glm::round(*this->worldPosition);
		// And finally transform it back to grid coordinates by transforming it with the
		// grid matrix.
		this->worldPosition = this->gridMatrix * glm::vec4{*this->worldPosition, 1};
	}
	/*
	if (this->worldPosition.has_value())
	{
		this->newStatusText("Position: (%1, %2, %3)"_q
			.arg(toDouble(this->worldPosition->x))
			.arg(toDouble(this->worldPosition->y))
			.arg(toDouble(this->worldPosition->z)));
	}
	else
	{
		this->newStatusText("Position: <none>"_q);
	}
	*/
	// use a relatively high threshold so that we know when the grid is somewhat perpendicular so we can
	// automatically change it properly
	if (isGridPerpendicularToScreen(0.03f))
	{
		const glm::vec3 cameraDirection = this->cameraVector();
		const glm::vec3 vector_x = glm::normalize(this->gridMatrix * glm::vec4{1, 0, 0, 1});
		const glm::vec3 vector_y = glm::normalize(this->gridMatrix * glm::vec4{0, 1, 0, 1});
		const float angle_x = std::abs(glm::dot(vector_x, cameraDirection));
		const float angle_y = std::abs(glm::dot(vector_y, cameraDirection));
		if (angle_x < angle_y)
		{
			this->newStatusText("rotate by X axis");
			this->gridMatrix = glm::rotate(this->gridMatrix, PIf / 2, glm::vec3{1, 0, 0});
		}
		else
		{
			this->newStatusText("rotate by Y axis");
			this->gridMatrix = glm::rotate(this->gridMatrix, PIf / 2, glm::vec3{0, 1, 0});
		}
		this->updateGridMatrix();
		this->update();
	}
	else
	{
		this->newStatusText("don't rotate");
	}
	PartRenderer::mouseMoveEvent(event);
}

void Canvas::mousePressEvent(QMouseEvent* event)
{
	this->totalMouseMove = 0;
	this->lastMousePosition = event->pos();
	PartRenderer::mousePressEvent(event);
}

void Canvas::mouseReleaseEvent(QMouseEvent* event)
{
	if (this->totalMouseMove < (2.0 / sqrt(2)) * 5.0)
	{
		if (this->highlighted == ldraw::NULL_ID)
		{
			this->selection = {};
		}
		else
		{
			this->selection = {this->highlighted};
		}
		this->compiler->setSelectedObjects(this->selection);
		emit selectionChanged(this->selection);
		this->update();
	}
	PartRenderer::mouseReleaseEvent(event);
}

void Canvas::initializeGL()
{
	// We first create the grid program and connect everything and only then call the part renderer's initialization
	// functions so that when initialization sets up, the signals also set up the matrices on our side.
	this->gridProgram.emplace(this);
	this->gridProgram->initialize();
	this->axesProgram.emplace(this);
	this->axesProgram->initialize();
	for (AbstractBasicShaderProgram* program : {
		static_cast<AbstractBasicShaderProgram*>(&*this->gridProgram),
		static_cast<AbstractBasicShaderProgram*>(&*this->axesProgram),
	})
	{
		connect(this, &PartRenderer::projectionMatrixChanged,
			program, &AbstractBasicShaderProgram::setProjectionMatrix);
		connect(this, &PartRenderer::modelMatrixChanged,
			program, &AbstractBasicShaderProgram::setModelMatrix);
		connect(this, &PartRenderer::viewMatrixChanged,
			program, &AbstractBasicShaderProgram::setViewMatrix);
	}
	connect(this, &PartRenderer::renderPreferencesChanged, [&]()
	{
		if (this->gridProgram.has_value())
		{
			const bool isDark = luma(this->renderPreferences.backgroundColor) < 0.25;
			this->gridProgram->setGridColor(isDark ? Qt::white : Qt::black);
		}
	});
	PartRenderer::initializeGL();
	// Set up XZ grid matrix
	this->gridMatrix = glm::mat4{
		{1, 0, 0, 0},
		{0, 0, 1, 0},
		{0, 1, 0, 0},
		{0, 0, 0, 1}
	};
	this->updateGridMatrix();
}

void Canvas::paintGL()
{
	PartRenderer::paintGL();
	// Render axes
	{
		glLineWidth(5);
		glEnable(GL_LINE_SMOOTH);
		glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
		this->axesProgram->draw();
		glDisable(GL_LINE_SMOOTH);
	}
	// Render grid
	{
		glEnable(GL_BLEND);
		glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
		this->gridProgram->draw();
		glDisable(GL_BLEND);
	}
	if (this->worldPosition.has_value())
	{
		QPainter painter{this};
		painter.setRenderHint(QPainter::Antialiasing);
		painter.setPen(Qt::black);
		painter.setBrush(Qt::green);
		const QPointF pos = this->modelToScreenCoordinates(*this->worldPosition);
		painter.drawEllipse(pos, 5, 5);
		painter.setPen(Qt::white);
		painter.drawText(pos + QPointF{5, 5}, vectorToString(*this->worldPosition));
	}
	{
		QPainter painter{this};
		QFont font;
		//font.setStyle(QFont::StyleItalic);
		painter.setFont(font);
		QFontMetrics fontMetrics{font};
		const auto renderText = [&](const QString& text, const geom::PointOnRectagle& intersection)
		{
			QPointF position = toQPointF(intersection.position);
			const geom::RectangleSide side = intersection.side;
			switch (side)
			{
			case geom::RectangleSide::Top:
				position += QPointF{0, static_cast<qreal>(fontMetrics.ascent())};
				break;
			case geom::RectangleSide::Left:
				break;
			case geom::RectangleSide::Bottom:
				position += QPointF{0, static_cast<qreal>(-fontMetrics.descent())};
				break;
			case geom::RectangleSide::Right:
				position += QPointF{static_cast<qreal>(-fontMetrics.width(text)), 0};
				break;
			}
			painter.drawText(position, text);
		};
		const QRectF box {
			QPointF{0, 0},
			QPointF{static_cast<qreal>(this->width()), static_cast<qreal>(this->height())}
		};
		const QPointF p1 = this->modelToScreenCoordinates(glm::vec3{0, 0, 0});

		static const struct
		{
			QString text;
			glm::vec3 direction;
		} directions[] =
		{
			{"+๐‘ฅ", {1, 0, 0}},
			{"-๐‘ฅ", {-1, 0, 0}},
			{"+๐‘ฆ", {0, 1, 0}},
			{"-๐‘ฆ", {0, -1, 0}},
			{"+๐‘ง", {0, 0, 1}},
			{"-๐‘ง", {0, 0, -1}},
		};
		for (const auto& axis : directions)
		{
			const QPointF x_p = this->modelToScreenCoordinates(axis.direction);
			const auto intersection = geom::rayRectangleIntersection(geom::rayFromPoints(toVec2(p1), toVec2(x_p)), box);
			if (intersection.has_value())
			{
				renderText(axis.text, *intersection);
			}
		}
	}
}

void Canvas::updateGridMatrix()
{
	const geom::Triangle triangle {
		this->gridMatrix * glm::vec4{0, 0, 0, 1},
		this->gridMatrix * glm::vec4{1, 0, 0, 1},
		this->gridMatrix * glm::vec4{0, 1, 0, 1},
	};
	this->gridPlane = geom::planeFromTriangle(triangle);
	this->gridProgram->setGridMatrix(this->gridMatrix);
}

glm::vec3 Canvas::cameraVector() const
{
	// Find out where the grid is projected on the screen
	const QPoint gridOrigin2d = pointFToPoint(this->modelToScreenCoordinates(this->gridPlane.anchor));
	// Find out which direction the camera is looking at the grid origin in 3d
	return glm::normalize(this->cameraLine(gridOrigin2d).direction);
}

/**
 * @brief Calculates if the screen is perpendicular to the current grid
 * @return yes no
 */
bool Canvas::isGridPerpendicularToScreen(float threshold) const
{
	const glm::vec3 cameraDirection = this->cameraVector();
	// Compute the dot product. The parameters given are:
	// - the normal of the grid plane, which is the vector from the grid origin perpendicular to the grid
	// - the direction of the camera looking at the grid, which is the inverse of the vector from the grid
	//   origin towards the camera
	// If the dot product between these two vectors is 0, the grid normal is perpendicular to the camera vector
	// and the grid is perpendicular to the screen.
	const float dot = glm::dot(glm::normalize(this->gridPlane.normal), glm::normalize(cameraDirection));
	return std::abs(dot) < threshold;
}

mercurial