src/ui/canvas.cpp

Wed, 09 Mar 2022 12:42:45 +0200

author
Teemu Piippo <teemu@hecknology.net>
date
Wed, 09 Mar 2022 12:42:45 +0200
changeset 172
50f055543ff6
parent 170
9b655f6fe5a1
child 187
30204975694a
permissions
-rw-r--r--

Render vertices as spheres

#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);
}

/**
 * @brief Handles a change of selection
 * @param selectedIds IDs of objects to select
 * @param deselectedIds IDs of objects to deselect.
 */
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();
}

/**
 * @brief Updates vertex rendering
 * @param document Document to get vertices from
 */
void Canvas::rebuildVertices(Document* document)
{
	if (this->vertexProgram.has_value())
	{
		this->vertexProgram->build(document);
		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};
	}
	Q_EMIT this->mouseMove(this, event);
	PartRenderer::mouseMoveEvent(event);
	this->update();
}

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)
	{
		Q_EMIT this->mouseClick(this, event);
	}
	PartRenderer::mouseReleaseEvent(event);
	this->update();
}

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();
	this->vertexProgram.emplace(this);
	this->vertexProgram->initialize();
	for (AbstractBasicShaderProgram* program : {
		static_cast<AbstractBasicShaderProgram*>(&*this->gridProgram),
		static_cast<AbstractBasicShaderProgram*>(&*this->axesProgram),
		static_cast<AbstractBasicShaderProgram*>(&*this->vertexProgram),
	})
	{
		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, this, &Canvas::updateCanvasRenderPreferences);
	PartRenderer::initializeGL();
	// Set up XZ grid matrix
	this->setGridMatrix({{1, 0, 0, 0}, {0, 0, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 1}});
	this->updateCanvasRenderPreferences();
}

void Canvas::paintGL()
{
	PartRenderer::paintGL();
	if (this->renderPreferences.style != gl::RenderStyle::PickScene)
	{
		// Render axes
		if (this->renderPreferences.drawAxes)
		{
			glLineWidth(5);
			glEnable(GL_LINE_SMOOTH);
			glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
			this->axesProgram->draw();
			glDisable(GL_LINE_SMOOTH);
		}
		// Render vertices
		{
			glCullFace(GL_FRONT);
			this->vertexProgram->draw();
		}
		// Render grid
		{
			glLineWidth(1);
			glEnable(GL_BLEND);
			glLineStipple(1, 0x8888);
			glEnable(GL_LINE_STIPPLE);
			glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
			this->gridProgram->draw();
			glDisable(GL_BLEND);
			glDisable(GL_LINE_STIPPLE);
		}
		if (this->worldPosition.has_value())
		{
			QPainter painter{this};
			painter.setRenderHint(QPainter::Antialiasing);
			painter.setPen(this->isDark ? Qt::white : Qt::black);
			painter.setBrush(Qt::green);
			const QPointF pos = this->modelToScreenCoordinates(*this->worldPosition);
			painter.drawEllipse(pos, 5, 5);
			painter.drawText(pos + QPointF{5, 5}, vectorToString(*this->worldPosition));
		}
		{
			QPainter painter{this};
			painter.setRenderHint(QPainter::Antialiasing);
			if (this->renderPreferences.drawAxes)
			{
				this->renderAxesLabels(painter);
			}
			if (this->overpaintCallback != nullptr)
			{
				this->overpaintCallback(this, &painter);
			}
		}
	}
}

/**
 * @brief Renders labels such as +x at the ends of axes at the screen
 * @param painter
 */
void Canvas::renderAxesLabels(QPainter& painter)
{
	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.horizontalAdvance(text)), 0};
			break;
		}
		painter.drawText(position, text);
	};
	const QRectF box {QPointF{0, 0}, sizeToSizeF(this->size())};
	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);
		}
	}
}

/**
 * @brief Draws a polyline to where the specified vector of 3D points would appear on the screen.
 * @param painter Painter to use to draw with
 * @param points 3D points to render
 */
void Canvas::drawWorldPolyline(QPainter *painter, const std::vector<glm::vec3> &points)
{
	painter->drawPolyline(QPolygonF{this->convertWorldPointsToScreenPoints(points)});
}

/**
 * @brief Draws a polygon to where the specified vector of 3D points would appear on the screen.
 * @param painter Painter to use to draw with
 * @param points 3D points to render
 */
void Canvas::drawWorldPolygon(QPainter* painter, const std::vector<glm::vec3> &points)
{
	painter->drawPolygon(QPolygonF{this->convertWorldPointsToScreenPoints(points)});
}

Winding Canvas::worldPolygonWinding(const std::vector<glm::vec3> &points) const
{
	return geom::winding(QPolygonF{this->convertWorldPointsToScreenPoints(points)});
}

/**
 * @brief Gets the current position of the cursor in the model
 * @return 3D vector
 */
const std::optional<glm::vec3>& Canvas::getWorldPosition() const
{
	return this->worldPosition;
}

/**
 * @brief Adjusts the grid to be so that it is perpendicular to the camera.
 */
void Canvas::adjustGridToView()
{
	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->setGridMatrix(glm::rotate(this->gridMatrix, PI<float> / 2, glm::vec3{1, 0, 0}));
	}
	else
	{
		this->setGridMatrix(glm::rotate(this->gridMatrix, PI<float> / 2, glm::vec3{0, 1, 0}));
	}
	this->update();
}

/**
 * @returns the ids of the currently selected objects 
 */
const QSet<ldraw::id_t> Canvas::selectedObjects() const
{
	return this->selection;
}

/**
 * @brief Paints a circle at where @c worldPoint is located on the screen.
 * @param painter Painter to use to render
 * @param worldPoint Point to render
 */
void Canvas::drawWorldPoint(QPainter* painter, const glm::vec3& worldPoint) const
{
	const QPointF center = this->modelToScreenCoordinates(worldPoint);
	painter->drawEllipse(geom::inscribe(geom::CircleF{center, 5}));
}

/**
 * @brief Changes the grid matrix to the one specified. Updates relevant member variables.
 * @param newMatrix New matrix to use
 */
void Canvas::setGridMatrix(const glm::mat4& newMatrix)
{
	this->gridMatrix = newMatrix;
	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);
}

/**
 * @brief Gets the current camera vector, i.e. the vector from the camera to the grid origin.
 * @return vector
 */
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 whether the screen is perpendicular to the current grid
 * @return bool
 */
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;
}

QVector<QPointF> Canvas::convertWorldPointsToScreenPoints(const std::vector<glm::vec3> &worldPoints) const
{
	QVector<QPointF> points2d;
	points2d.reserve(worldPoints.size());
	for (const glm::vec3& point : worldPoints)
	{
		points2d.push_back(this->modelToScreenCoordinates(point));
	}
	return points2d;
}

void Canvas::updateCanvasRenderPreferences()
{
	this->isDark = luma(this->renderPreferences.backgroundColor) < 0.25;
	if (this->gridProgram.has_value())
	{
		this->gridProgram->setGridColor(this->isDark ? Qt::white : Qt::black);
	}
}

/**
 * @brief Clears the selection.
 */
void Canvas::clearSelection()
{
	this->selection.clear();
	this->compiler->setSelectedObjects(this->selection);
	Q_EMIT selectionChanged(this->selection);
	this->update();
}

/**
 * @brief Adds an object to selection.
 * @param id ID of object to add
 */
void Canvas::addToSelection(ldraw::id_t id)
{
	this->selection.insert(id);
	this->compiler->setSelectedObjects(this->selection);
	Q_EMIT selectionChanged(this->selection);
	this->update();
}

void Canvas::setOverpaintCallback(Canvas::OverpaintCallback fn)
{
	this->overpaintCallback = fn;
}

mercurial