src/document.cpp

Tue, 14 Jun 2022 17:55:50 +0300

author
Teemu Piippo <teemu.s.piippo@gmail.com>
date
Tue, 14 Jun 2022 17:55:50 +0300
changeset 217
6d95c1a41e6e
parent 214
8e1fe64ce4e3
child 222
72b456f2f3c2
permissions
-rw-r--r--

reimplement EditTools as a render layer

/*
 *  LDForge: LDraw parts authoring CAD
 *  Copyright (C) 2013 - 2020 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/>.
 */

#include <QMouseEvent>
#include <QPainter>
#include "document.h"
#include "model.h"
#include "ui/objecteditor.h"
#include "gl/partrenderer.h"

EditTools::EditTools(QObject* parent) :
	QObject{parent},
	RenderLayer{}
{
}

EditTools::~EditTools()
{
}

void EditTools::setEditMode(EditingMode mode)
{
	this->mode = mode;
}

void EditTools::setGridMatrix(const glm::mat4& gridMatrix)
{
	this->gridMatrix = gridMatrix;
	this->gridPlane = planeFromTriangle({
		this->gridMatrix * glm::vec4{0, 0, 0, 1},
		this->gridMatrix * glm::vec4{1, 0, 0, 1},
		this->gridMatrix * glm::vec4{0, 1, 0, 1},
	});
}

void EditTools::mvpMatrixChanged(const glm::mat4& matrix)
{
	this->mvpMatrix = matrix;
}

void EditTools::mouseMoved(const QMouseEvent* event)
{
	this->worldPosition = this->renderer->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};
	}
	this->updatePreviewPolygon();
}

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

static Winding worldPolygonWinding(
	const std::vector<glm::vec3> &points,
	const PartRenderer* renderer)
{
	return winding(QPolygonF{convertWorldPointsToScreenPoints(points, renderer)});
}

static void drawWorldPoint(
	QPainter* painter,
	const glm::vec3& worldPoint,
	const PartRenderer* renderer)
{
	const QPointF center = renderer->modelToScreenCoordinates(worldPoint);
	painter->drawEllipse(inscribe(CircleF{center, 5}));
}

static void drawWorldPolyline(
	QPainter *painter,
	const std::vector<glm::vec3> &points,
	const PartRenderer* renderer)
{
	painter->drawPolyline(QPolygonF{convertWorldPointsToScreenPoints(points, renderer)});
}

static void drawWorldPolygon(
	QPainter* painter,
	const std::vector<glm::vec3> &points,
	const PartRenderer* renderer)
{
	painter->drawPolygon(QPolygonF{convertWorldPointsToScreenPoints(points, renderer)});
}

void EditTools::overpaint(QPainter* painter)
{
	struct Pens
	{
		const QBrush pointBrush;
		const QPen pointPen;
		const QPen polygonPen;
		const QPen badPolygonPen;
		const QBrush greenPolygonBrush;
		const QBrush redPolygonBrush;
	};
	static const Pens brightPens{
		.pointBrush = {Qt::white},
		.pointPen = {QBrush{Qt::black}, 2.0},
		.polygonPen = {QBrush{Qt::black}, 2.0, Qt::DashLine},
		.badPolygonPen = {QBrush{Qt::red}, 2.0, Qt::DashLine},
		.greenPolygonBrush = {QColor{64, 255, 128, 192}},
		.redPolygonBrush = {QColor{255, 96, 96, 192}},
	};
	static const Pens darkPens{
		.pointBrush = {Qt::black},
		.pointPen = {QBrush{Qt::white}, 2.0},
		.polygonPen = {QBrush{Qt::white}, 2.0, Qt::DashLine},
		.badPolygonPen = {QBrush{Qt::red}, 2.0, Qt::DashLine},
		.greenPolygonBrush = {QColor{64, 255, 128, 192}},
		.redPolygonBrush = {QColor{255, 96, 96, 192}},
	};
	const Pens& pens = (this->renderer->isDark() ? darkPens : brightPens);
	switch(this->mode) {
	case SelectMode:
		break;
	case DrawMode:
		{
			painter->setPen(this->isconcave ? pens.badPolygonPen : pens.polygonPen);
			if (this->previewPolygon.size() > 2 and not this->isconcave)
			{
				if (worldPolygonWinding(this->previewPolygon, this->renderer) == Winding::Clockwise) {
					painter->setBrush(pens.greenPolygonBrush);
				}
				else {
					painter->setBrush(pens.redPolygonBrush);
				}
				drawWorldPolygon(painter, this->previewPolygon, this->renderer);
			}
			else {
				drawWorldPolyline(painter, this->previewPolygon, this->renderer);
			}
			painter->setBrush(pens.pointBrush);
			painter->setPen(pens.pointPen);
			for (const glm::vec3& point : this->polygon) {
				drawWorldPoint(painter, point, this->renderer);
			}
		}
		break;
	}
	if (this->worldPosition.has_value())
	{
		painter->setRenderHint(QPainter::Antialiasing);
		painter->setPen(Qt::white);
		painter->setBrush(Qt::green);
		const QPointF pos = this->renderer->modelToScreenCoordinates(*this->worldPosition);
		painter->drawEllipse(pos, 5, 5);
		painter->drawText(pos + QPointF{5, 5}, vectorToString(*this->worldPosition));
	}
}

void EditTools::updatePreviewPolygon()
{
	this->previewPolygon = this->polygon;
	if (this->worldPosition.has_value()) {
		this->previewPolygon.resize(this->polygon.size() + 1);
		this->previewPolygon.back() = *this->worldPosition;
	}
	if (this->previewPolygon.size() > 2)
	{
		this->isconcave = not isConvex(this->previewPolygon);
	}
}

void EditTools::removeLastPoint()
{
	if (this->polygon.size() > 0)
	{
		this->polygon.erase(this->polygon.end() - 1);
		this->updatePreviewPolygon();
	}
}

bool isCloseToExistingPoints(const std::vector<glm::vec3>& points, const glm::vec3 &pos)
{
	return any(points, std::bind(isclose, std::placeholders::_1, pos));
}

EditingMode EditTools::currentEditingMode() const
{
	return this->mode;
}

void EditTools::mouseClick(const QMouseEvent* event)
{
	switch(this->mode) {
	case SelectMode:
		if (event->button() == Qt::LeftButton) {
			const ModelId highlighted = this->renderer->pick(event->pos());
			Q_EMIT this->select({highlighted}, false);
		}
		break;
	case DrawMode:
		if (event->button() == Qt::LeftButton and this->worldPosition.has_value()) {
			if (isCloseToExistingPoints(this->polygon, *this->worldPosition)) {
				this->closeShape();
			}
			else {
				this->polygon.push_back(*this->worldPosition);
				this->updatePreviewPolygon();
			}
		}
		else if (true
			and event->button() == Qt::RightButton
			and this->polygon.size() > 0
		) {
			this->polygon.erase(this->polygon.end() - 1);
			updatePreviewPolygon();
		}
		break;
	}
}

void EditTools::closeShape()
{
	if (this->polygon.size() >= 2 and this->polygon.size() <= 4) {
		switch (this->polygon.size()) {
		case 2:
			Q_EMIT this->modelAction(AppendToModel{
				.newElement = Colored<LineSegment>{
					LineSegment{
						.p1 = this->polygon[0],
						.p2 = this->polygon[1],
					},
					EDGE_COLOR,
				}
			});
			break;
		case 3:
			Q_EMIT this->modelAction(AppendToModel{
				.newElement = Colored<Triangle>{
					Triangle{
						.p1 = this->polygon[0],
						.p2 = this->polygon[1],
						.p3 = this->polygon[2],
					},
					MAIN_COLOR,
				}
			});
			break;
		case 4:
			Q_EMIT this->modelAction(AppendToModel{
				.newElement = Colored<Quadrilateral>{
					Quadrilateral{
						.p1 = this->polygon[0],
						.p2 = this->polygon[1],
						.p3 = this->polygon[2],
						.p4 = this->polygon[3],
					},
					MAIN_COLOR,
				}
			});
			break;
		}
	}
	this->polygon.clear();
	this->updatePreviewPolygon();
}

mercurial