Move render layer files to new src/layers/ directory

Sun, 26 Jun 2022 21:00:06 +0300

author
Teemu Piippo <teemu.s.piippo@gmail.com>
date
Sun, 26 Jun 2022 21:00:06 +0300
changeset 263
59b6027b9843
parent 262
dc33f8a707c4
child 264
76a025db4948

Move render layer files to new src/layers/ directory

.hgignore file | annotate | diff | comparison | revisions
CMakeLists.txt file | annotate | diff | comparison | revisions
src/document.cpp file | annotate | diff | comparison | revisions
src/document.h file | annotate | diff | comparison | revisions
src/gl/axesprogram.cpp file | annotate | diff | comparison | revisions
src/gl/axesprogram.h file | annotate | diff | comparison | revisions
src/gl/gridprogram.cpp file | annotate | diff | comparison | revisions
src/gl/gridprogram.h file | annotate | diff | comparison | revisions
src/layers/axeslayer.cpp file | annotate | diff | comparison | revisions
src/layers/axeslayer.h file | annotate | diff | comparison | revisions
src/layers/edittools.cpp file | annotate | diff | comparison | revisions
src/layers/edittools.h file | annotate | diff | comparison | revisions
src/layers/gridlayer.cpp file | annotate | diff | comparison | revisions
src/layers/gridlayer.h file | annotate | diff | comparison | revisions
src/main.cpp file | annotate | diff | comparison | revisions
src/ui/objecteditor.cpp file | annotate | diff | comparison | revisions
src/widgets/colorindexinput.h file | annotate | diff | comparison | revisions
--- a/.hgignore	Sun Jun 26 20:54:09 2022 +0300
+++ b/.hgignore	Sun Jun 26 21:00:06 2022 +0300
@@ -1,5 +1,5 @@
 syntax:glob
-CMakeLists.txt.user
+CMakeLists.txt.user*
 __pycache__
 *.orig
 debug
--- a/CMakeLists.txt	Sun Jun 26 20:54:09 2022 +0300
+++ b/CMakeLists.txt	Sun Jun 26 21:00:06 2022 +0300
@@ -27,7 +27,6 @@
 
 set (LDFORGE_SOURCES
 	src/colors.cpp
-	src/document.cpp
 	src/documentmanager.cpp
 	src/geometry.cpp
 	src/ldrawalgorithm.cpp
@@ -41,13 +40,14 @@
 	src/uiutilities.cpp
 	src/version.cpp
 	src/vertexmap.cpp
-	src/gl/axesprogram.cpp
 	src/gl/basicshaderprogram.cpp
 	src/gl/compiler.cpp
 	src/gl/debug.cpp
-	src/gl/gridprogram.cpp
 	src/gl/partrenderer.cpp
 #	src/gl/vertexprogram.cpp
+	src/layers/axeslayer.cpp
+	src/layers/edittools.cpp
+	src/layers/gridlayer.cpp
 	src/settingseditor/keyboardshortcutseditor.cpp
 	src/settingseditor/librarieseditor.cpp
 	src/settingseditor/settingseditor.cpp
@@ -61,7 +61,6 @@
 	src/basics.h
 	src/circularprimitive.h
 	src/colors.h
-	src/document.h
 	src/documentmanager.h
 	src/functional.h
 	src/geometry.h
@@ -79,14 +78,15 @@
 	src/version.h
 	src/vertexmap.h
 	src/algorithm/earcut.h
-	src/gl/axesprogram.h
 	src/gl/basicshaderprogram.h
 	src/gl/common.h
 	src/gl/compiler.h
 	src/gl/debug.h
-	src/gl/gridprogram.h
 	src/gl/partrenderer.h
 #	src/gl/vertexprogram.h
+	src/layers/axeslayer.h
+	src/layers/edittools.h
+	src/layers/gridlayer.h
 	src/settingseditor/keyboardshortcutseditor.h
 	src/settingseditor/librarieseditor.h
 	src/settingseditor/settingseditor.h
--- a/src/document.cpp	Sun Jun 26 20:54:09 2022 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,459 +0,0 @@
-/*
- *  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 "algorithm/earcut.h"
-#include "document.h"
-#include "model.h"
-#include "ui/objecteditor.h"
-#include "gl/partrenderer.h"
-#include "circularprimitive.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}));
-}
--- a/src/document.h	Sun Jun 26 20:54:09 2022 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,78 +0,0 @@
-/*
- *  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/>.
- */
-
-#pragma once
-#include <memory>
-#include <QWidget>
-#include <QToolBar>
-#include "model.h"
-#include "vertexmap.h"
-#include "gl/common.h"
-
-enum EditingMode
-{
-	SelectMode,
-	DrawMode,
-	CircleMode
-};
-
-Q_DECLARE_METATYPE(EditingMode)
-
-Q_DECLARE_METATYPE(ModelAction)
-
-class EditTools final : public QObject, public RenderLayer
-{
-	Q_OBJECT
-	std::vector<glm::vec3> polygon = {{0, 0, 0}};
-	std::size_t numpoints = 1;
-	EditingMode mode = SelectMode;
-	glm::mat4 mvpMatrix;
-	glm::mat4 gridMatrix{1};
-	Plane gridPlane;
-	opt<glm::vec3> worldPosition;
-	CircleToolOptions circleToolOptions = {
-		.fraction = {16, 16},
-		.type = CircularPrimitive::Circle,
-	};
-public:
-	explicit EditTools(QObject *parent = nullptr);
-	~EditTools() override;
-	void applyToVertices(VertexMap::ApplyFunction fn) const;
-	const QSet<ModelId> selectedObjects() const;
-	EditingMode currentEditingMode() const;
-	Q_SLOT void setEditMode(EditingMode mode);
-	Q_SLOT void setGridMatrix(const glm::mat4& gridMatrix);
-	Q_SLOT void setCircleToolOptions(const CircleToolOptions& options);
-Q_SIGNALS:
-	void newStatusText(const QString& newStatusText);
-	void modelAction(const ModelAction& action);
-	void select(const QSet<ModelId>& ids, bool retain);
-protected:
-	void mvpMatrixChanged(const glm::mat4& matrix) override;
-	void mouseMoved(const QMouseEvent* event) override;
-	void mouseClick(const QMouseEvent* event) override;
-	void overpaint(QPainter* painter) override;
-private:
-	const std::vector<ModelAction> modelActions() const;
-	const std::vector<ModelAction> circleModeActions() const;
-	const std::vector<ModelAction> drawModeActions() const;
-	void closeShape();
-	void renderPreview(QPainter* painter, const void* pensptr);
-	void removeLastPoint();
-	bool isCloseToExistingPoints() const;
-};
--- a/src/gl/axesprogram.cpp	Sun Jun 26 20:54:09 2022 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,162 +0,0 @@
-/*
- *  LDForge: LDraw parts authoring CAD
- *  Copyright (C) 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 "gl/partrenderer.h"
-#include <QPainter>
-#include <QPainterPath>
-#include "axesprogram.h"
-
-static constexpr char vertexShaderSource[] = R"(
-#version 330 core
-
-layout (location = 0) in vec3 in_position;
-layout (location = 1) in vec3 in_color;
-uniform mat4 mvp;
-smooth out vec3 ex_color;
-
-void main()
-{
-	gl_Position = mvp * vec4(in_position, 1.0);
-	ex_color = in_color;
-}
-)";
-
-static constexpr char fragmentShaderSource[] = R"(
-#version 330 core
-
-out vec4 color;
-smooth in vec3 ex_color;
-
-void main(void)
-{
-	color = vec4(ex_color, 1);
-}
-)";
-
-void AxesLayer::initializeGL()
-{
-	constexpr struct VertexType
-	{
-		glm::vec3 position;
-		glm::vec3 color;
-	} data[] = {
-		{{10000, 0, 0}, {1, 0, 0}},
-		{{0, 0, 0}, {1, 0, 0}},
-		{{-10000, 0, 0}, {0.5, 0, 0}},
-		{{0, 0, 0}, {0.5, 0, 0}},
-		{{0, 10000, 0}, {0, 1, 0}},
-		{{0, 0, 0}, {0, 1, 0}},
-		{{0, -10000, 0}, {0, 0.5, 0}},
-		{{0, 0, 0}, {0, 0.5, 0}},
-		{{0, 0, 10000}, {0, 0, 1}},
-		{{0, 0, 0}, {0, 0, 1}},
-		{{0, 0, -10000}, {0, 0, 0.5}},
-		{{0, 0, 0}, {0, 0, 0.5}},
-	};
-	constexpr int stride = sizeof(VertexType);
-	this->shader.initialize(
-		::vertexShaderSource,
-		::fragmentShaderSource,
-		QOpenGLBuffer::StaticDraw,
-		{
-			GLAttributeSpec{
-				.type = GL_FLOAT,
-				.offset = offsetof(VertexType, position),
-				.tuplesize = 3,
-				.stride = stride,
-			},
-			{
-				.type = GL_FLOAT,
-				.offset = offsetof(VertexType, color),
-				.tuplesize = 3,
-				.stride = stride,
-			},
-		});
-	this->shader.bufferData(&data[0], countof(data), sizeof data[0]);
-}
-
-void AxesLayer::overpaint(QPainter* painter)
-{
-	painter->save();
-	QFont font;
-	font.setStyle(QFont::StyleItalic);
-	font.setBold(true);
-	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;
-		}
-		drawBorderedText(painter, position, font, text);
-	};
-	const QRectF box {QPointF{0, 0}, sizeToSizeF(this->renderer->size())};
-	const QPointF p1 = this->renderer->modelToScreenCoordinates(glm::vec3{0, 0, 0});
-	static const struct
-	{
-		QString text;
-		glm::vec3 direction;
-	} directions[] =
-	{
-		{"+x", {1, 0, 0}},
-		{"-x", {-1, 0, 0}},
-		{"+y", {0, 1, 0}},
-		{"-y", {0, -1, 0}},
-		{"+z", {0, 0, 1}},
-		{"-z", {0, 0, -1}},
-	};
-	for (const auto& axis : directions)
-	{
-		const QPointF x_p = this->renderer->modelToScreenCoordinates(axis.direction);
-		const auto intersection = rayRectangleIntersection(
-			rayFromPoints(toVec2(p1), toVec2(x_p)),
-			box);
-		if (intersection.has_value())
-		{
-			renderText(axis.text, *intersection);
-		}
-	}
-	painter->restore();
-}
-
-void AxesLayer::paintGL()
-{
-	glLineWidth(5);
-	glEnable(GL_LINE_SMOOTH);
-	glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
-	this->shader.draw(GL_LINES);
-	glDisable(GL_LINE_SMOOTH);
-}
-
-void AxesLayer::mvpMatrixChanged(const glm::mat4& mvpMatrix)
-{
-	this->shader.setMvpMatrix(mvpMatrix);
-}
--- a/src/gl/axesprogram.h	Sun Jun 26 20:54:09 2022 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,13 +0,0 @@
-#pragma once
-#include "gl/common.h"
-#include "gl/basicshaderprogram.h"
-
-class AxesLayer final : public RenderLayer
-{
-	BasicShader shader;
-public:
-	void initializeGL() override;
-	void overpaint(QPainter* painter) override;
-	void paintGL() override;
-	void mvpMatrixChanged(const glm::mat4& mvpMatrix) override;
-};
--- a/src/gl/gridprogram.cpp	Sun Jun 26 20:54:09 2022 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,129 +0,0 @@
-/*
- *  LDForge: LDraw parts authoring CAD
- *  Copyright (C) 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 "gridprogram.h"
-#include "partrenderer.h"
-
-constexpr char vertexShaderSource[] = R"(
-#version 330 core
-
-layout (location = 0) in vec2 in_position;
-uniform mat4 mvp;
-smooth out vec2 ex_uv;
-uniform mat4 grid;
-
-void main()
-{
-	gl_Position = mvp * grid * vec4(in_position, 0.0, 1.0);
-	ex_uv = in_position;
-}
-)";
-
-constexpr char fragmentShaderSource[] = R"(
-#version 330 core
-
-out vec4 color;
-smooth in vec2 ex_uv;
-uniform vec4 gridColor;
-
-void main(void)
-{
-	float dx = fract(ex_uv.y);
-	float dy = fract(ex_uv.x);
-	/* fade the grid towards extreme co-ordinates */
-	float d = (1.0f - 0.015 * max(abs(ex_uv.x), abs(ex_uv.y)));
-	color = vec4(gridColor.xyz, gridColor.w * d);
-}
-)";
-
-template<int extent>
-constexpr auto calcGridData()
-{
-	std::array<glm::vec2, 8 * extent + 4> result;
-	std::size_t ix = 0;
-	for (int i = -extent; i <= extent; i += 1) {
-		result[ix++] = {i, -extent};
-		result[ix++] = {i, extent};
-	}
-	for (int i = -extent; i <= extent; i += 1) {
-		result[ix++] = {-extent, i};
-		result[ix++] = {extent, i};
-	}
-	return result;
-}
-
-void GridLayer::setGridMatrix(const glm::mat4& newGridMatrix)
-{
-	this->gridMatrix = newGridMatrix;
-	if (this->isInitialized) {
-		this->shader.setUniformMatrix("grid", newGridMatrix);
-	}
-}
-
-void GridLayer::setGridColor(const QColor& newGridColor)
-{
-	this->gridColor = gl::colorToVector4(newGridColor);
-	if (this->isInitialized) {
-		this->shader.setUniformVector("gridColor", this->gridColor);
-	}
-}
-
-void GridLayer::settingsChanged()
-{
-	this->setGridColor(this->renderer->isDark() ? Qt::white : Qt::black);
-}
-
-void GridLayer::initializeGL()
-{
-	this->shader.initialize(
-		::vertexShaderSource,
-		::fragmentShaderSource,
-		QOpenGLBuffer::StaticDraw,
-		{
-			GLAttributeSpec{
-				.type = GL_FLOAT,
-				.offset = 0,
-				.tuplesize = 2,
-				.stride = 0,
-			},
-		}
-	);
-	this->isInitialized = true;
-	constexpr auto data = calcGridData<50>();
-	this->shader.setUniformVector("gridColor", this->gridColor);
-	this->setGridMatrix(this->gridMatrix);
-	this->settingsChanged();
-	this->shader.bufferData(data.data(), data.size(), sizeof data[0]);
-}
-
-void GridLayer::paintGL()
-{
-	glLineWidth(1);
-	glEnable(GL_BLEND);
-	glLineStipple(1, 0x8888);
-	glEnable(GL_LINE_STIPPLE);
-	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
-	this->shader.draw(GL_LINES);
-	glDisable(GL_BLEND);
-	glDisable(GL_LINE_STIPPLE);
-}
-
-void GridLayer::mvpMatrixChanged(const glm::mat4& mvpMatrix)
-{
-	this->shader.setMvpMatrix(mvpMatrix);
-}
--- a/src/gl/gridprogram.h	Sun Jun 26 20:54:09 2022 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,37 +0,0 @@
-/*
- *  LDForge: LDraw parts authoring CAD
- *  Copyright (C) 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/>.
- */
-
-#pragma once
-#include "basicshaderprogram.h"
-#include "common.h"
-
-class GridLayer final : public RenderLayer
-{
-	BasicShader shader;
-	glm::vec4 gridColor = {1.0f, 1.0f, 1.0f, 0.75f};
-	glm::mat4 gridMatrix{1};
-	bool isInitialized = false;
-public:
-	void setGridMatrix(const glm::mat4& newGridMatrix);
-	void setGridColor(const QColor& newGridColor);
-	void settingsChanged();
-protected:
-	void initializeGL() override;
-	void paintGL() override;
-	void mvpMatrixChanged(const glm::mat4& mvpMatrix) override;
-};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/layers/axeslayer.cpp	Sun Jun 26 21:00:06 2022 +0300
@@ -0,0 +1,162 @@
+/*
+ *  LDForge: LDraw parts authoring CAD
+ *  Copyright (C) 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 "gl/partrenderer.h"
+#include <QPainter>
+#include <QPainterPath>
+#include "axeslayer.h"
+
+static constexpr char vertexShaderSource[] = R"(
+#version 330 core
+
+layout (location = 0) in vec3 in_position;
+layout (location = 1) in vec3 in_color;
+uniform mat4 mvp;
+smooth out vec3 ex_color;
+
+void main()
+{
+	gl_Position = mvp * vec4(in_position, 1.0);
+	ex_color = in_color;
+}
+)";
+
+static constexpr char fragmentShaderSource[] = R"(
+#version 330 core
+
+out vec4 color;
+smooth in vec3 ex_color;
+
+void main(void)
+{
+	color = vec4(ex_color, 1);
+}
+)";
+
+void AxesLayer::initializeGL()
+{
+	constexpr struct VertexType
+	{
+		glm::vec3 position;
+		glm::vec3 color;
+	} data[] = {
+		{{10000, 0, 0}, {1, 0, 0}},
+		{{0, 0, 0}, {1, 0, 0}},
+		{{-10000, 0, 0}, {0.5, 0, 0}},
+		{{0, 0, 0}, {0.5, 0, 0}},
+		{{0, 10000, 0}, {0, 1, 0}},
+		{{0, 0, 0}, {0, 1, 0}},
+		{{0, -10000, 0}, {0, 0.5, 0}},
+		{{0, 0, 0}, {0, 0.5, 0}},
+		{{0, 0, 10000}, {0, 0, 1}},
+		{{0, 0, 0}, {0, 0, 1}},
+		{{0, 0, -10000}, {0, 0, 0.5}},
+		{{0, 0, 0}, {0, 0, 0.5}},
+	};
+	constexpr int stride = sizeof(VertexType);
+	this->shader.initialize(
+		::vertexShaderSource,
+		::fragmentShaderSource,
+		QOpenGLBuffer::StaticDraw,
+		{
+			GLAttributeSpec{
+				.type = GL_FLOAT,
+				.offset = offsetof(VertexType, position),
+				.tuplesize = 3,
+				.stride = stride,
+			},
+			{
+				.type = GL_FLOAT,
+				.offset = offsetof(VertexType, color),
+				.tuplesize = 3,
+				.stride = stride,
+			},
+		});
+	this->shader.bufferData(&data[0], countof(data), sizeof data[0]);
+}
+
+void AxesLayer::overpaint(QPainter* painter)
+{
+	painter->save();
+	QFont font;
+	font.setStyle(QFont::StyleItalic);
+	font.setBold(true);
+	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;
+		}
+		drawBorderedText(painter, position, font, text);
+	};
+	const QRectF box {QPointF{0, 0}, sizeToSizeF(this->renderer->size())};
+	const QPointF p1 = this->renderer->modelToScreenCoordinates(glm::vec3{0, 0, 0});
+	static const struct
+	{
+		QString text;
+		glm::vec3 direction;
+	} directions[] =
+	{
+		{"+x", {1, 0, 0}},
+		{"-x", {-1, 0, 0}},
+		{"+y", {0, 1, 0}},
+		{"-y", {0, -1, 0}},
+		{"+z", {0, 0, 1}},
+		{"-z", {0, 0, -1}},
+	};
+	for (const auto& axis : directions)
+	{
+		const QPointF x_p = this->renderer->modelToScreenCoordinates(axis.direction);
+		const auto intersection = rayRectangleIntersection(
+			rayFromPoints(toVec2(p1), toVec2(x_p)),
+			box);
+		if (intersection.has_value())
+		{
+			renderText(axis.text, *intersection);
+		}
+	}
+	painter->restore();
+}
+
+void AxesLayer::paintGL()
+{
+	glLineWidth(5);
+	glEnable(GL_LINE_SMOOTH);
+	glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
+	this->shader.draw(GL_LINES);
+	glDisable(GL_LINE_SMOOTH);
+}
+
+void AxesLayer::mvpMatrixChanged(const glm::mat4& mvpMatrix)
+{
+	this->shader.setMvpMatrix(mvpMatrix);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/layers/axeslayer.h	Sun Jun 26 21:00:06 2022 +0300
@@ -0,0 +1,13 @@
+#pragma once
+#include "gl/common.h"
+#include "gl/basicshaderprogram.h"
+
+class AxesLayer final : public RenderLayer
+{
+	BasicShader shader;
+public:
+	void initializeGL() override;
+	void overpaint(QPainter* painter) override;
+	void paintGL() override;
+	void mvpMatrixChanged(const glm::mat4& mvpMatrix) override;
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/layers/edittools.cpp	Sun Jun 26 21:00:06 2022 +0300
@@ -0,0 +1,459 @@
+/*
+ *  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 "../algorithm/earcut.h"
+#include "../model.h"
+#include "../ui/objecteditor.h"
+#include "../gl/partrenderer.h"
+#include "../circularprimitive.h"
+#include "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}));
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/layers/edittools.h	Sun Jun 26 21:00:06 2022 +0300
@@ -0,0 +1,78 @@
+/*
+ *  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/>.
+ */
+
+#pragma once
+#include <memory>
+#include <QWidget>
+#include <QToolBar>
+#include "model.h"
+#include "vertexmap.h"
+#include "gl/common.h"
+
+enum EditingMode
+{
+	SelectMode,
+	DrawMode,
+	CircleMode
+};
+
+Q_DECLARE_METATYPE(EditingMode)
+
+Q_DECLARE_METATYPE(ModelAction)
+
+class EditTools final : public QObject, public RenderLayer
+{
+	Q_OBJECT
+	std::vector<glm::vec3> polygon = {{0, 0, 0}};
+	std::size_t numpoints = 1;
+	EditingMode mode = SelectMode;
+	glm::mat4 mvpMatrix;
+	glm::mat4 gridMatrix{1};
+	Plane gridPlane;
+	opt<glm::vec3> worldPosition;
+	CircleToolOptions circleToolOptions = {
+		.fraction = {16, 16},
+		.type = CircularPrimitive::Circle,
+	};
+public:
+	explicit EditTools(QObject *parent = nullptr);
+	~EditTools() override;
+	void applyToVertices(VertexMap::ApplyFunction fn) const;
+	const QSet<ModelId> selectedObjects() const;
+	EditingMode currentEditingMode() const;
+	Q_SLOT void setEditMode(EditingMode mode);
+	Q_SLOT void setGridMatrix(const glm::mat4& gridMatrix);
+	Q_SLOT void setCircleToolOptions(const CircleToolOptions& options);
+Q_SIGNALS:
+	void newStatusText(const QString& newStatusText);
+	void modelAction(const ModelAction& action);
+	void select(const QSet<ModelId>& ids, bool retain);
+protected:
+	void mvpMatrixChanged(const glm::mat4& matrix) override;
+	void mouseMoved(const QMouseEvent* event) override;
+	void mouseClick(const QMouseEvent* event) override;
+	void overpaint(QPainter* painter) override;
+private:
+	const std::vector<ModelAction> modelActions() const;
+	const std::vector<ModelAction> circleModeActions() const;
+	const std::vector<ModelAction> drawModeActions() const;
+	void closeShape();
+	void renderPreview(QPainter* painter, const void* pensptr);
+	void removeLastPoint();
+	bool isCloseToExistingPoints() const;
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/layers/gridlayer.cpp	Sun Jun 26 21:00:06 2022 +0300
@@ -0,0 +1,129 @@
+/*
+ *  LDForge: LDraw parts authoring CAD
+ *  Copyright (C) 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 "../gl/partrenderer.h"
+#include "gridlayer.h"
+
+constexpr char vertexShaderSource[] = R"(
+#version 330 core
+
+layout (location = 0) in vec2 in_position;
+uniform mat4 mvp;
+smooth out vec2 ex_uv;
+uniform mat4 grid;
+
+void main()
+{
+	gl_Position = mvp * grid * vec4(in_position, 0.0, 1.0);
+	ex_uv = in_position;
+}
+)";
+
+constexpr char fragmentShaderSource[] = R"(
+#version 330 core
+
+out vec4 color;
+smooth in vec2 ex_uv;
+uniform vec4 gridColor;
+
+void main(void)
+{
+	float dx = fract(ex_uv.y);
+	float dy = fract(ex_uv.x);
+	/* fade the grid towards extreme co-ordinates */
+	float d = (1.0f - 0.015 * max(abs(ex_uv.x), abs(ex_uv.y)));
+	color = vec4(gridColor.xyz, gridColor.w * d);
+}
+)";
+
+template<int extent>
+constexpr auto calcGridData()
+{
+	std::array<glm::vec2, 8 * extent + 4> result;
+	std::size_t ix = 0;
+	for (int i = -extent; i <= extent; i += 1) {
+		result[ix++] = {i, -extent};
+		result[ix++] = {i, extent};
+	}
+	for (int i = -extent; i <= extent; i += 1) {
+		result[ix++] = {-extent, i};
+		result[ix++] = {extent, i};
+	}
+	return result;
+}
+
+void GridLayer::setGridMatrix(const glm::mat4& newGridMatrix)
+{
+	this->gridMatrix = newGridMatrix;
+	if (this->isInitialized) {
+		this->shader.setUniformMatrix("grid", newGridMatrix);
+	}
+}
+
+void GridLayer::setGridColor(const QColor& newGridColor)
+{
+	this->gridColor = gl::colorToVector4(newGridColor);
+	if (this->isInitialized) {
+		this->shader.setUniformVector("gridColor", this->gridColor);
+	}
+}
+
+void GridLayer::settingsChanged()
+{
+	this->setGridColor(this->renderer->isDark() ? Qt::white : Qt::black);
+}
+
+void GridLayer::initializeGL()
+{
+	this->shader.initialize(
+		::vertexShaderSource,
+		::fragmentShaderSource,
+		QOpenGLBuffer::StaticDraw,
+		{
+			GLAttributeSpec{
+				.type = GL_FLOAT,
+				.offset = 0,
+				.tuplesize = 2,
+				.stride = 0,
+			},
+		}
+	);
+	this->isInitialized = true;
+	constexpr auto data = calcGridData<50>();
+	this->shader.setUniformVector("gridColor", this->gridColor);
+	this->setGridMatrix(this->gridMatrix);
+	this->settingsChanged();
+	this->shader.bufferData(data.data(), data.size(), sizeof data[0]);
+}
+
+void GridLayer::paintGL()
+{
+	glLineWidth(1);
+	glEnable(GL_BLEND);
+	glLineStipple(1, 0x8888);
+	glEnable(GL_LINE_STIPPLE);
+	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+	this->shader.draw(GL_LINES);
+	glDisable(GL_BLEND);
+	glDisable(GL_LINE_STIPPLE);
+}
+
+void GridLayer::mvpMatrixChanged(const glm::mat4& mvpMatrix)
+{
+	this->shader.setMvpMatrix(mvpMatrix);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/layers/gridlayer.h	Sun Jun 26 21:00:06 2022 +0300
@@ -0,0 +1,37 @@
+/*
+ *  LDForge: LDraw parts authoring CAD
+ *  Copyright (C) 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/>.
+ */
+
+#pragma once
+#include "../gl/basicshaderprogram.h"
+#include "../gl/common.h"
+
+class GridLayer final : public RenderLayer
+{
+	BasicShader shader;
+	glm::vec4 gridColor = {1.0f, 1.0f, 1.0f, 0.75f};
+	glm::mat4 gridMatrix{1};
+	bool isInitialized = false;
+public:
+	void setGridMatrix(const glm::mat4& newGridMatrix);
+	void setGridColor(const QColor& newGridColor);
+	void settingsChanged();
+protected:
+	void initializeGL() override;
+	void paintGL() override;
+	void mvpMatrixChanged(const glm::mat4& mvpMatrix) override;
+};
--- a/src/main.cpp	Sun Jun 26 20:54:09 2022 +0300
+++ b/src/main.cpp	Sun Jun 26 21:00:06 2022 +0300
@@ -8,10 +8,10 @@
 #include "mainwindow.h"
 #include "ui_mainwindow.h"
 #include "version.h"
-#include "gl/axesprogram.h"
-#include "gl/gridprogram.h"
 #include "gl/partrenderer.h"
-#include "document.h"
+#include "layers/axeslayer.h"
+#include "layers/gridlayer.h"
+#include "layers/edittools.h"
 #include "settingseditor/settingseditor.h"
 #include "widgets/colorselectdialog.h"
 #include "settings.h"
--- a/src/ui/objecteditor.cpp	Sun Jun 26 20:54:09 2022 +0300
+++ b/src/ui/objecteditor.cpp	Sun Jun 26 21:00:06 2022 +0300
@@ -3,7 +3,6 @@
 #include <QFormLayout>
 #include <widgets/vec3editor.h>
 #include "objecteditor.h"
-#include "document.h"
 #include "widgets/colorbutton.h"
 #include "widgets/colorindexinput.h"
 #include "ui_objecteditor.h"
--- a/src/widgets/colorindexinput.h	Sun Jun 26 20:54:09 2022 +0300
+++ b/src/widgets/colorindexinput.h	Sun Jun 26 21:00:06 2022 +0300
@@ -1,5 +1,5 @@
 #pragma once
-#include "document.h"
+#include "colors.h"
 
 class ColorIndexInput : public QWidget
 {

mercurial