Tue, 14 Jun 2022 17:55:50 +0300
reimplement EditTools as a render layer
/* * 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 "document.h" #include "model.h" #include "ui/objecteditor.h" #include "gl/partrenderer.h" EditTools::EditTools(QObject* parent) : QObject{parent}, RenderLayer{} { } EditTools::~EditTools() { } void EditTools::setEditMode(EditingMode mode) { this->mode = mode; } void EditTools::setGridMatrix(const glm::mat4& gridMatrix) { this->gridMatrix = gridMatrix; 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::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->updatePreviewPolygon(); } static QVector<QPointF> convertWorldPointsToScreenPoints( const std::vector<glm::vec3> &worldPoints, const PartRenderer* renderer) { QVector<QPointF> points2d; points2d.reserve(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)}); } void EditTools::overpaint(QPainter* painter) { struct Pens { const QBrush pointBrush; const QPen pointPen; const QPen polygonPen; const QPen badPolygonPen; const QBrush greenPolygonBrush; const QBrush redPolygonBrush; }; static const Pens brightPens{ .pointBrush = {Qt::white}, .pointPen = {QBrush{Qt::black}, 2.0}, .polygonPen = {QBrush{Qt::black}, 2.0, Qt::DashLine}, .badPolygonPen = {QBrush{Qt::red}, 2.0, Qt::DashLine}, .greenPolygonBrush = {QColor{64, 255, 128, 192}}, .redPolygonBrush = {QColor{255, 96, 96, 192}}, }; static const Pens darkPens{ .pointBrush = {Qt::black}, .pointPen = {QBrush{Qt::white}, 2.0}, .polygonPen = {QBrush{Qt::white}, 2.0, Qt::DashLine}, .badPolygonPen = {QBrush{Qt::red}, 2.0, Qt::DashLine}, .greenPolygonBrush = {QColor{64, 255, 128, 192}}, .redPolygonBrush = {QColor{255, 96, 96, 192}}, }; const Pens& pens = (this->renderer->isDark() ? darkPens : brightPens); switch(this->mode) { case SelectMode: break; case DrawMode: { painter->setPen(this->isconcave ? pens.badPolygonPen : pens.polygonPen); if (this->previewPolygon.size() > 2 and not this->isconcave) { if (worldPolygonWinding(this->previewPolygon, this->renderer) == Winding::Clockwise) { painter->setBrush(pens.greenPolygonBrush); } else { painter->setBrush(pens.redPolygonBrush); } drawWorldPolygon(painter, this->previewPolygon, this->renderer); } else { drawWorldPolyline(painter, this->previewPolygon, this->renderer); } painter->setBrush(pens.pointBrush); painter->setPen(pens.pointPen); for (const glm::vec3& point : this->polygon) { drawWorldPoint(painter, point, this->renderer); } } break; } if (this->worldPosition.has_value()) { painter->setRenderHint(QPainter::Antialiasing); painter->setPen(Qt::white); painter->setBrush(Qt::green); const QPointF pos = this->renderer->modelToScreenCoordinates(*this->worldPosition); painter->drawEllipse(pos, 5, 5); painter->drawText(pos + QPointF{5, 5}, vectorToString(*this->worldPosition)); } } void EditTools::updatePreviewPolygon() { this->previewPolygon = this->polygon; if (this->worldPosition.has_value()) { this->previewPolygon.resize(this->polygon.size() + 1); this->previewPolygon.back() = *this->worldPosition; } if (this->previewPolygon.size() > 2) { this->isconcave = not isConvex(this->previewPolygon); } } void EditTools::removeLastPoint() { if (this->polygon.size() > 0) { this->polygon.erase(this->polygon.end() - 1); this->updatePreviewPolygon(); } } bool isCloseToExistingPoints(const std::vector<glm::vec3>& points, const glm::vec3 &pos) { return any(points, std::bind(isclose, std::placeholders::_1, pos)); } 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->polygon, *this->worldPosition)) { this->closeShape(); } else { this->polygon.push_back(*this->worldPosition); this->updatePreviewPolygon(); } } else if (true and event->button() == Qt::RightButton and this->polygon.size() > 0 ) { this->polygon.erase(this->polygon.end() - 1); updatePreviewPolygon(); } break; } } void EditTools::closeShape() { if (this->polygon.size() >= 2 and this->polygon.size() <= 4) { switch (this->polygon.size()) { case 2: Q_EMIT this->modelAction(AppendToModel{ .newElement = Colored<LineSegment>{ LineSegment{ .p1 = this->polygon[0], .p2 = this->polygon[1], }, EDGE_COLOR, } }); break; case 3: Q_EMIT this->modelAction(AppendToModel{ .newElement = Colored<Triangle>{ Triangle{ .p1 = this->polygon[0], .p2 = this->polygon[1], .p3 = this->polygon[2], }, MAIN_COLOR, } }); break; case 4: Q_EMIT this->modelAction(AppendToModel{ .newElement = Colored<Quadrilateral>{ Quadrilateral{ .p1 = this->polygon[0], .p2 = this->polygon[1], .p3 = this->polygon[2], .p4 = this->polygon[3], }, MAIN_COLOR, } }); break; } } this->polygon.clear(); this->updatePreviewPolygon(); }