src/ui/canvas.cpp

changeset 296
38f6fad61bad
parent 295
4241d948af28
child 297
bc92f97498f7
--- a/src/ui/canvas.cpp	Tue Jun 28 19:25:45 2022 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,431 +0,0 @@
-#include <QMouseEvent>
-#include <QPainter>
-#include "document.h"
-#include "canvas.h"
-
-Canvas::Canvas(
-	Model* model,
-	EditTools *document,
-	DocumentManager* documents,
-	const ColorTable& colorTable,
-	QWidget* parent) :
-	PartRenderer{model, documents, colorTable, parent},
-	document{document}
-{
-	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<ModelId> &selectedIds, const QSet<ModelId> &deselectedIds)
-{
-	Q_ASSERT(not selectedIds.contains({0}));
-	this->selection.subtract(deselectedIds);
-	this->selection.unite(selectedIds);
-	gl::setModelShaderSelectedObjects(&this->shaders, this->selection);
-	this->update();
-}
-
-/**
- * @brief Updates vertex rendering
- * @param document Document to get vertices from
- */
-void Canvas::rebuildVertices(VertexMap* vertexMap)
-{
-	if (this->vertexProgram.has_value())
-	{
-		this->vertexProgram->build(vertexMap);
-		this->update();
-	}
-}
-
-void Canvas::mouseMoveEvent(QMouseEvent* event)
-{
-	const ModelId 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(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(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();
-}
-
-static const struct
-{
-	const QBrush pointBrush = {Qt::white};
-	const QPen polygonPen = {QBrush{Qt::black}, 2.0, Qt::DashLine};
-	const QPen badPolygonPen = {QBrush{Qt::red}, 2.0, Qt::DashLine};
-	const QPen pointPen = {QBrush{Qt::black}, 2.0};
-	const QBrush greenPolygonBrush = {QColor{64, 255, 128, 192}};
-	const QBrush redPolygonBrush = {QColor{255, 96, 96, 192}};
-} pens;
-
-static void renderDrawState(
-	QPainter* painter,
-	Canvas* canvas,
-	DrawState* drawState);
-
-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->drawState != nullptr) {
-			renderDrawState(&painter, this, this->drawState);
-		}
-	}
-}
-
-static void renderDrawState(
-	QPainter* painter,
-	Canvas* canvas,
-	DrawState* drawState)
-{
-	switch(drawState->mode)
-	{
-	case SelectMode:
-		break;
-	case DrawMode:
-		{
-			painter->setPen(drawState->isconcave ? ::pens.badPolygonPen : ::pens.polygonPen);
-			if (drawState->previewPolygon.size() > 2 and not drawState->isconcave)
-			{
-				if (canvas->worldPolygonWinding(drawState->previewPolygon) == Winding::Clockwise)
-				{
-					painter->setBrush(::pens.greenPolygonBrush);
-				}
-				else
-				{
-					painter->setBrush(::pens.redPolygonBrush);
-				}
-				canvas->drawWorldPolygon(painter, drawState->previewPolygon);
-			}
-			else
-			{
-				canvas->drawWorldPolyline(painter, drawState->previewPolygon);
-			}
-			painter->setBrush(::pens.pointBrush);
-			painter->setPen(::pens.pointPen);
-			for (const glm::vec3& point : drawState->polygon)
-			{
-				canvas->drawWorldPoint(painter, point);
-			}
-			canvas->drawWorldPoint(painter, drawState->previewPoint);
-		}
-		break;
-	}
-}
-
-/**
- * @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 PointOnRectagle& intersection)
-	{
-		QPointF position = toQPointF(intersection.position);
-		const RectangleSide side = intersection.side;
-		switch (side)
-		{
-		case RectangleSide::Top:
-			position += QPointF{0, static_cast<qreal>(fontMetrics.ascent())};
-			break;
-		case RectangleSide::Left:
-			break;
-		case RectangleSide::Bottom:
-			position += QPointF{0, static_cast<qreal>(-fontMetrics.descent())};
-			break;
-		case 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 = rayRectangleIntersection(
-			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 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 adjustGridToView(Canvas* canvas)
-{
-	const glm::vec3 cameraDirection = canvas->cameraVector();
-	const glm::mat4& grid = canvas->getGridMatrix();
-	const glm::vec3 vector_x = glm::normalize(grid * glm::vec4{1, 0, 0, 1});
-	const glm::vec3 vector_y = glm::normalize(grid * 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));
-	canvas->setGridMatrix(glm::rotate(
-		grid,
-		pi<> * 0.5f,
-		(angle_x < angle_y) ? glm::vec3{1, 0, 0} : glm::vec3{0, 1, 0}
-	));
-	canvas->update();
-}
-
-/**
- * @returns the ids of the currently selected objects 
- */
-const QSet<ModelId> Canvas::selectedObjects() const
-{
-	return this->selection;
-}
-
-const glm::mat4 &Canvas::getGridMatrix() const
-{
-	return this->gridMatrix;
-}
-
-/**
- * @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(inscribe(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 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 = planeFromTriangle(triangle);
-	this->gridProgram->setGridMatrix(this->gridMatrix);
-	this->update();
-}
-
-/**
- * @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 QPointF gridOrigin2d = 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);
-	}
-}
-
-void Canvas::setOverpaintCallback(Canvas::OverpaintCallback fn)
-{
-	this->overpaintCallback = fn;
-}

mercurial