src/layers/edittools.cpp

Sun, 26 Jun 2022 21:32:51 +0300

author
Teemu Piippo <teemu.s.piippo@gmail.com>
date
Sun, 26 Jun 2022 21:32:51 +0300
changeset 264
76a025db4948
parent 263
59b6027b9843
child 301
8ccd6fdb30dc
permissions
-rw-r--r--

Convert all includes to be relative to project root directory. Files that cannot be found in this manner use angle brackets.

/*
 *  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 "src/algorithm/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"

// 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;
}

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

static std::vector<std::vector<glm::vec3>> modelActionPoints(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(*circ, [&](const ModelElement& element){
				const auto& subpoints = modelActionPoints(AppendToModel{element});
				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();
	const Pens& pens = (this->renderer->isDark() ? darkPens : brightPens);
	this->renderPreview(painter, &pens);
	QFont font;
	font.setBold(true);
	if (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 : modelActionPoints(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() - 1);
	}
}

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 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->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{
			.newElement = Colored<LineSegment>{
				LineSegment{
					.p1 = this->polygon[0],
					.p2 = this->polygon[1],
				},
				EDGE_COLOR,
			}
		});
	}
	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});
		}		
		using indextype = std::uint16_t;
		const std::vector<indextype> indices = mapbox::earcut<std::uint16_t>(polygons);
		MergedTriangles mergedTriangles = mergeTriangles(indices, this->polygon);
		for (const Quadrilateral& quad : mergedTriangles.quadrilaterals) {
			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()) {
				result.push_back(AppendToModel{
					.newElement = Colored<Triangle>{
						Triangle{
							.p1 = this->polygon[indices[i]],
							.p2 = this->polygon[indices[i + 1]],
							.p3 = this->polygon[indices[i + 2]],
						},
						MAIN_COLOR,
					}
				});
			}
		}
	}
	return result;
}

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