src/layers/edittools.cpp

Sun, 09 Apr 2023 17:05:40 +0300

author
Teemu Piippo <teemu.s.piippo@gmail.com>
date
Sun, 09 Apr 2023 17:05:40 +0300
changeset 368
9444de9762c3
parent 333
07e65a4c6611
child 372
b2914aaeec1a
permissions
-rw-r--r--

Simplify signature of openModelFromPath

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

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);
	this->localPosition = event->localPos();
	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->inputPolygon.updateCurrentPoint(*this->worldPosition);
	}
}

//! \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 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->inputPolygon) {
		drawWorldPoint(painter, point, this->renderer);
	}
	if (this->mode == CircleMode and this->inputPolygon.polygonSize() >= 2) {
		const glm::vec3 circleOrigin = this->inputPolygon[0];
		const QPointF originScreen = this->renderer->modelToScreenCoordinates(circleOrigin);
		const auto extremity = [this, &originScreen](const glm::vec3& p){
			const QPointF s2 = this->renderer->modelToScreenCoordinates(p);
			const auto intersection = rayRectangleIntersection(
				rayFromPoints(toVec2(originScreen), toVec2(s2)),
				this->renderer->rect());
			if (intersection.has_value()) {
				return intersection->position;
			}
			else {
				return glm::vec2{s2.x(), s2.y()};
			}
		};
		const glm::vec3 zvec = this->gridMatrix[2];
		if (this->inputPolygon.bufferSize() >= 3) {
			const glm::vec2 p1 = extremity(this->inputPolygon[0] + zvec);
			const glm::vec2 p2 = extremity(this->inputPolygon[0] - zvec);
			const glm::vec2 lateral = glm::normalize(glm::mat2{{0, 1}, {-1, 0}} * (p2 - p1));
			painter->setPen(QPen{Qt::white, 3});
			painter->drawLine(vecToQPoint(p1), vecToQPoint(p2));
			constexpr float notchsize = 40.0f;
			for (int a = -30; a <= 30; ++a) {
				const glm::vec3 notch = this->inputPolygon[0] + static_cast<float>(a) * zvec;
				const QPointF s_notchcenter = this->renderer->modelToScreenCoordinates(notch);
				const QPointF notch_s1 = s_notchcenter + notchsize * 0.5f * vecToQPoint(lateral);
				const QPointF notch_s2 = s_notchcenter - notchsize * 0.5f * vecToQPoint(lateral);
				painter->drawLine(notch_s1, notch_s2);
			}
			const opt<float> height = this->cylinderHeight();
			if (height.has_value()) {
				const glm::vec3 heightvec = height.value_or(0) * zvec;
				const glm::vec3 p = this->inputPolygon[1] + 0.5f * heightvec;
				QFont font{};
				font.setBold(true);
				drawBorderedText(painter, this->renderer->modelToScreenCoordinates(p), font, QString::number(*height));
			}
		}
	}
}

opt<float> EditTools::cylinderHeight() const
{
	if (this->inputPolygon.bufferSize() < 3) {
		return {};
	}
	else {
		const glm::vec3 cameravec = glm::normalize(this->renderer->cameraVector(this->localPosition));
		const glm::vec3 heightvec = glm::normalize(glm::vec3{gridMatrix[2]});
		const glm::vec3 normal = glm::cross(glm::cross(cameravec, heightvec), heightvec);
		const Plane plane{
			.normal = normal,
			.anchor = this->inputPolygon[0],
		};
		const opt<glm::vec3> p = this->renderer->screenToModelCoordinates(this->localPosition, plane);
		if (p.has_value()) {
			return std::round(glm::dot(*p - this->inputPolygon[0], heightvec));
		}
		else {
			return {};
		}
	}
}

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

void EditTools::mouseClick(const QMouseEvent* event)
{
	switch(this->mode) {
	case SelectMode:
		if (event->button() == Qt::LeftButton) {
			const std::int32_t 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 (this->inputPolygon.currentPointOnExistingPoint()) {
				this->closeShape();
			}
			else {
				this->inputPolygon.finishCurrentPoint();
			}
		}
		break;
	case CircleMode:
		if (event->button() == Qt::LeftButton) {
			if (this->inputPolygon.bufferSize() == 3) {
				this->closeShape();
			}
			else if (this->worldPosition.has_value()) {
				this->inputPolygon.finishCurrentPoint();
			}
		}
		break;
	}
	if (event->button() == Qt::RightButton) {
		this->inputPolygon.removeLastPoint();
	}
}


const std::vector<ModelAction> EditTools::circleModeActions() const
{
	std::vector<ModelAction> result;
	if (this->inputPolygon.polygonSize() >= 2) {
		const glm::vec3 x = this->inputPolygon[1] - this->inputPolygon[0];
		const opt<float> cyliheight = this->cylinderHeight().value_or(1);
		glm::mat4 transform{
			glm::vec4{x, 0},
			*cyliheight * this->gridMatrix[2],
			glm::vec4{glm::cross(glm::vec3{-this->gridMatrix[2]}, x), 0},
			glm::vec4{this->inputPolygon[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->inputPolygon.polygonSize() == 2) {
		result.push_back(AppendToModel{edge(this->inputPolygon[0], this->inputPolygon[1])});
	}
	else if (this->inputPolygon.polygonSize() > 2) {
		for (const PlainPolygonElement& poly : polygonize(
			this->inputPolygon.begin(),
			this->inputPolygon.polygonEnd())
		) {
			result.push_back(AppendToModel{
				.newElement = elementFromPolygonAndColor(poly, MAIN_COLOR),
			});
		}
	}
	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->inputPolygon.clear();
}

mercurial