src/layers/edittools.cpp

Fri, 01 Jul 2022 16:46:43 +0300

author
Teemu Piippo <teemu.s.piippo@gmail.com>
date
Fri, 01 Jul 2022 16:46:43 +0300
changeset 312
2637134bc37c
parent 311
fab454611f9b
child 314
4642ba1218e8
permissions
-rw-r--r--

Fix right click to delete not really working properly
Instead of removing the point that had been added, it would remove
the point that is being drawn, which would cause it to overwrite the
previous point using the new point, causing a bit of a delay

/*
 *  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 "thirdparty/earcut.h"
#include "src/model.h"
#include "src/ui/objecteditor.h"
#include "src/gl/partrenderer.h"
#include "src/circularprimitive.h"
#include "src/layers/edittools.h"
#include "src/invert.h"

// Make mapbox::earcut work with glm::vec3
template<> struct mapbox::util::nth<0, glm::vec3>
{
	static constexpr float get(const glm::vec3& t) { return t.x; }
};

template<> struct mapbox::util::nth<1, glm::vec3>
{
	static constexpr float get(const glm::vec3& t) { return t.y; }
};

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

EditTools::~EditTools()
{
}

void EditTools::setEditMode(EditingMode newMode)
{
	this->mode = newMode;
	switch (this->mode) {
	case SelectMode:
		Q_EMIT this->suggestCursor(Qt::ArrowCursor);
		break;
	case DrawMode:
	case CircleMode:
		Q_EMIT this->suggestCursor(Qt::CrossCursor);
		break;
	}
}

void EditTools::setGridMatrix(const glm::mat4& newGridMatrix)
{
	this->gridMatrix = newGridMatrix;
	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::setCircleToolOptions(const CircleToolOptions& options)
{
	this->circleToolOptions = options;
}

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->polygon.back() = *this->worldPosition;
	}
	this->numpoints = this->polygon.size();
	if (this->isCloseToExistingPoints()) {
		this->numpoints -= 1;
	}
}

static QVector<QPointF> convertWorldPointsToScreenPoints(
	const std::vector<glm::vec3> &worldPoints,
	const PartRenderer* renderer)
{
	QVector<QPointF> points2d;
	points2d.reserve(static_cast<int>(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)});
}

//! \brief Conversion function from PlainPolygonElement to ModelElement
ModelElement elementFromPolygonAndColor(const PlainPolygonElement& poly, ColorIndex color)
{
	// use std::visit with a templated lambda to resolve the type of poly.
	return std::visit([color](const auto& resolvedPoly) -> ModelElement {
		// unlike with normal templates we need to pry out the type out manually
		using PolygonType = std::decay_t<decltype(resolvedPoly)>;
		// add color and return as a model element.
		return Colored<PolygonType>{resolvedPoly, color};
	}, poly);
}

static std::vector<std::vector<glm::vec3>> polygonsToBeInserted(const ModelAction& action)
{
	std::vector<std::vector<glm::vec3>> result;
	if (const AppendToModel* append = std::get_if<AppendToModel>(&action)) {
		const ModelElement& newElement = append->newElement;
		if (const LineSegment* seg = std::get_if<Colored<LineSegment>>(&newElement)) {
			result.push_back({seg->p1, seg->p2});
		}
		else if (const Triangle* tri = std::get_if<Colored<Triangle>>(&newElement)) {
			result.push_back({tri->p1, tri->p2, tri->p3});
		}
		else if (const Quadrilateral* quad = std::get_if<Colored<Quadrilateral>>(&newElement)) {
			result.push_back({quad->p1, quad->p2, quad->p3, quad->p4});
		}
		else if (const CircularPrimitive* circ = std::get_if<Colored<CircularPrimitive>>(&newElement)) {
			// rasterize the circle down to polygons, and append them to the result.
			rasterize(*circ, [&](const PlainPolygonElement& poly, const ColorIndex color){
				AppendToModel append{elementFromPolygonAndColor(poly, color)};
				const auto& subpoints = polygonsToBeInserted(append);
				std::copy(subpoints.begin(), subpoints.end(), std::back_inserter(result));
			});
		}
	}
	return result;
}

namespace {
struct Pens
{
	const QBrush pointBrush;
	const QPen pointPen;
	const QPen textPen;
	const QPen polygonPen;
	const QPen badPolygonPen;
	const QBrush greenPolygonBrush;
	const QBrush redPolygonBrush;
};
}

static const Pens brightPens{
	.pointBrush = {Qt::black},
	.pointPen = {QBrush{Qt::black}, 2.0},
	.textPen = {Qt::black},
	.polygonPen = {QBrush{Qt::black}, 2.0, Qt::DashLine},
	.greenPolygonBrush = {QColor{64, 255, 128, 192}},
	.redPolygonBrush = {QColor{255, 96, 96, 192}},
};

static const Pens darkPens{
	.pointBrush = {Qt::white},
	.pointPen = {QBrush{Qt::white}, 2.0},
	.textPen = {Qt::white},
	.polygonPen = {QBrush{Qt::white}, 2.0, Qt::DashLine},
	.greenPolygonBrush = {QColor{64, 255, 128, 192}},
	.redPolygonBrush = {QColor{255, 96, 96, 192}},
};

void EditTools::overpaint(QPainter* painter)
{
	painter->save();
	if (this->usePolygon()) {
		const Pens& pens = (this->renderer->isDark() ? darkPens : brightPens);
		this->renderPreview(painter, &pens);
		QFont font;
		font.setBold(true);
		if (this->usePolygon() and this->worldPosition.has_value())
		{
			painter->setRenderHint(QPainter::Antialiasing);
			painter->setPen(pens.pointPen);
			painter->setBrush(pens.greenPolygonBrush);
			const QPointF pos = this->renderer->modelToScreenCoordinates(*this->worldPosition);
			painter->drawEllipse(pos, 5, 5);
			drawBorderedText(painter, pos + QPointF{5, 5}, font, vectorToString(*this->worldPosition));
		}
	}
	painter->restore();
}

const std::vector<ModelAction> EditTools::modelActions() const
{
	switch(this->mode) {
	case SelectMode:
		return {};
	case DrawMode:
		return drawModeActions();
	case CircleMode:
		return circleModeActions();
	}
}

void EditTools::renderPreview(QPainter* painter, const void* pensptr)
{
	const Pens& pens = *reinterpret_cast<const Pens*>(pensptr);
	painter->setPen(pens.polygonPen);
	for (const ModelAction& action : this->modelActions()) {
		for (const std::vector<glm::vec3>& points : polygonsToBeInserted(action)) {
			if (points.size() == 2) {
				drawWorldPolyline(painter, points, renderer);
			}
			else {
				if (worldPolygonWinding(points, this->renderer) == Winding::Clockwise) {
					painter->setBrush(pens.greenPolygonBrush);
				}
				else {
					painter->setBrush(pens.redPolygonBrush);
				}
				drawWorldPolygon(painter, points, this->renderer);
			}
		}
	}
	painter->setBrush(pens.pointBrush);
	painter->setPen(pens.pointPen);
	for (const glm::vec3& point : this->polygon) {
		drawWorldPoint(painter, point, this->renderer);
	}
}

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

bool EditTools::isCloseToExistingPoints() const
{
	if (this->worldPosition.has_value()) {
		const glm::vec3& pos = *this->worldPosition;
		return std::any_of(this->polygon.begin(), this->polygon.end() - 1, [&pos](const glm::vec3& p){
			return isclose(pos, p);
		});
	}
	else {
		return false;
	}
}

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

void EditTools::mouseClick(const QMouseEvent* event)
{
	switch(this->mode) {
	case SelectMode:
		if (event->button() == Qt::LeftButton) {
			const ElementId 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->closeShape();
			}
			else {
				this->polygon.push_back(*this->worldPosition);
			}
		}
		break;
	case CircleMode:
		if (event->button() == Qt::LeftButton and this->worldPosition.has_value()) {
			if (this->polygon.size() == 2) {
				this->closeShape();
			}
			else {
				this->polygon.push_back(*this->worldPosition);
			}
		}
		break;
	}
	if (event->button() == Qt::RightButton and this->polygon.size() > 1) {
		this->removeLastPoint();
	}
}

struct MergedTriangles
{
	std::vector<Quadrilateral> quadrilaterals;
	std::set<std::size_t> cutTriangles;
};

static MergedTriangles mergeTriangles(
	const std::vector<std::uint16_t>& indices,
	const std::vector<glm::vec3>& polygon)
{
	MergedTriangles result;
	using indextype = std::uint16_t;
	using indexpair = std::pair<indextype, indextype>;
	struct boundaryinfo { indextype third; std::size_t triangleid; };
	std::map<indexpair, boundaryinfo> boundaries;
	for (std::size_t i = 0; i < indices.size(); i += 3) {
		const auto add = [&](const std::size_t o1, const std::size_t o2, const std::size_t o3){
			const auto key = std::make_pair(indices[i + o1], indices[i + o2]);
			boundaries[key] = {indices[i + o3], i};
		};
		add(0, 1, 2);
		add(1, 2, 0);
		add(2, 0, 1);
	}
	std::vector<std::array<indextype, 4>> quadindices;
	std::vector<Quadrilateral> quads;
	bool repeat = true;
	const auto iscut = [&result](const std::size_t i){
		return result.cutTriangles.find(i) != result.cutTriangles.end();
	};
	while (repeat) {
		repeat = false;
		// Go through triangle boundaries
		for (const auto& it1 : boundaries) {
			const indexpair& pair1 = it1.first;
			const boundaryinfo& boundary1 = it1.second;
			// .. the ones we haven't already merged anyway
			if (not iscut(boundary1.triangleid)) {
				// Look for its inverse boundary to find the touching triangle
				const auto pair2 = std::make_pair(pair1.second, pair1.first);
				const auto it2 = boundaries.find(pair2);
				// Also if that hasn't been cut
				if (it2 != boundaries.end() and not iscut(it2->second.triangleid)) {
					const Quadrilateral quad{
						polygon[pair1.first],
						polygon[it2->second.third],
						polygon[pair1.second],
						polygon[boundary1.third],
					};
					if (isConvex(quad)) {
						result.quadrilaterals.push_back(quad);
						result.cutTriangles.insert(boundary1.triangleid);
						result.cutTriangles.insert(it2->second.triangleid);
						repeat = true;
					}
				}
			}
		}
	}
	return result;
}


const std::vector<ModelAction> EditTools::circleModeActions() const
{
	std::vector<ModelAction> result;
	if (this->numpoints == 2) {
		const glm::vec3 x = polygon[1] - polygon[0];
		glm::mat4 transform{
			glm::vec4{x, 0},
			this->gridMatrix[2],
			glm::vec4{glm::cross(glm::vec3{-this->gridMatrix[2]}, x), 0},
			glm::vec4{this->polygon[0], 1},
		};
		Colored<CircularPrimitive> circ{
			CircularPrimitive{
				.type = this->circleToolOptions.type,
				.fraction = this->circleToolOptions.fraction,
				.transformation = transform,
			},
			MAIN_COLOR
		};
		result.push_back(AppendToModel{.newElement = circ});
	}
	return result;
}

const std::vector<ModelAction> EditTools::drawModeActions() const
{
	std::vector<ModelAction> result;
	if (this->numpoints == 2) {
		result.push_back(AppendToModel{edge(this->polygon[0], this->polygon[1])});
	}
	else if (this->numpoints > 2) {
		const glm::mat4 inverseGrid = glm::inverse(this->gridMatrix);
		std::vector<std::vector<glm::vec3>> polygons{1};
		std::vector<glm::vec3>& polygon2d = polygons.back();
		polygon2d.reserve(this->numpoints);
		for (std::size_t i = 0; i < this->numpoints; ++i) {
			polygon2d.push_back(inverseGrid * glm::vec4{this->polygon[i], 1});
		}
		// mapbox::earcut will always produce a CCW polygon, so if we're drawing
		// a CW polygon, we should invert the result afterwards
		const float shouldInvert = glm::dot(
			glm::vec3{inverseGrid[2]},
			glm::cross(this->polygon[0] - this->polygon[1], this->polygon[2] - this->polygon[1]));
		using indextype = std::uint16_t;
		const std::vector<indextype> indices = mapbox::earcut<std::uint16_t>(polygons);
		MergedTriangles mergedTriangles = mergeTriangles(indices, this->polygon);
		for (Quadrilateral& quad : mergedTriangles.quadrilaterals) {
			if (shouldInvert < 0) {
				invert(quad);
			}
			result.push_back(AppendToModel{
				.newElement = Colored<Quadrilateral>{quad, MAIN_COLOR},
			});
		}
		for (std::size_t i = 0; i < indices.size(); i += 3) {
			if (mergedTriangles.cutTriangles.find(i) == mergedTriangles.cutTriangles.end()) {
				Colored<Triangle> tri = triangle(
					this->polygon[indices[i]],
					this->polygon[indices[i + 1]],
					this->polygon[indices[i + 2]]);
				if (shouldInvert < 0) {
					invert(tri);
				}
				result.push_back(AppendToModel{tri});
			}
		}
	}
	return result;
}

bool EditTools::usePolygon() const
{
	switch (this->mode) {
	case SelectMode:
		return false;
	case DrawMode:
	case CircleMode:
		return true;
	}
	return {};
}

void EditTools::closeShape()
{
	for (const ModelAction& action : this->modelActions()) {
		Q_EMIT this->modelAction(action);
	}
	this->polygon.clear();
	this->polygon.push_back(this->worldPosition.value_or(glm::vec3{0, 0, 0}));
}

mercurial