diff -r dc33f8a707c4 -r 59b6027b9843 src/layers/edittools.cpp
--- /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 .
+ */
+
+#include
+#include
+#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 convertWorldPointsToScreenPoints(
+ const std::vector &worldPoints,
+ const PartRenderer* renderer)
+{
+ QVector points2d;
+ points2d.reserve(static_cast(worldPoints.size()));
+ for (const glm::vec3& point : worldPoints)
+ {
+ points2d.push_back(renderer->modelToScreenCoordinates(point));
+ }
+ return points2d;
+}
+
+static Winding worldPolygonWinding(
+ const std::vector &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 &points,
+ const PartRenderer* renderer)
+{
+ painter->drawPolyline(QPolygonF{convertWorldPointsToScreenPoints(points, renderer)});
+}
+
+static void drawWorldPolygon(
+ QPainter* painter,
+ const std::vector &points,
+ const PartRenderer* renderer)
+{
+ painter->drawPolygon(QPolygonF{convertWorldPointsToScreenPoints(points, renderer)});
+}
+
+static std::vector> modelActionPoints(const ModelAction& action)
+{
+ std::vector> result;
+ if (const AppendToModel* append = std::get_if(&action)) {
+ const ModelElement& newElement = append->newElement;
+ if (const LineSegment* seg = std::get_if>(&newElement)) {
+ result.push_back({seg->p1, seg->p2});
+ }
+ else if (const Triangle* tri = std::get_if>(&newElement)) {
+ result.push_back({tri->p1, tri->p2, tri->p3});
+ }
+ else if (const Quadrilateral* quad = std::get_if>(&newElement)) {
+ result.push_back({quad->p1, quad->p2, quad->p3, quad->p4});
+ }
+ else if (const CircularPrimitive* circ = std::get_if>(&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 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(pensptr);
+ painter->setPen(pens.polygonPen);
+ for (const ModelAction& action : this->modelActions()) {
+ for (const std::vector& 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 quadrilaterals;
+ std::set cutTriangles;
+};
+
+static MergedTriangles mergeTriangles(
+ const std::vector& indices,
+ const std::vector& polygon)
+{
+ MergedTriangles result;
+ using indextype = std::uint16_t;
+ using indexpair = std::pair;
+ struct boundaryinfo { indextype third; std::size_t triangleid; };
+ std::map 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> quadindices;
+ std::vector 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 EditTools::circleModeActions() const
+{
+ std::vector 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 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 EditTools::drawModeActions() const
+{
+ std::vector result;
+ if (this->numpoints == 2) {
+ result.push_back(AppendToModel{
+ .newElement = Colored{
+ 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> polygons{1};
+ std::vector& 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 indices = mapbox::earcut(polygons);
+ MergedTriangles mergedTriangles = mergeTriangles(indices, this->polygon);
+ for (const Quadrilateral& quad : mergedTriangles.quadrilaterals) {
+ result.push_back(AppendToModel{
+ .newElement = Colored{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{
+ .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}));
+}