Wed, 29 Jun 2022 14:43:42 +0300
Enable drawing clockwise shapes despite mapbox::earcut rewinding them
/* * 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 "thirdparty/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" #include "src/invert.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)}); } //! \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 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 : 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->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}); } // mapbox::earcut will always produce a CCW polygon, so if we're drawing // a CW polygon, we should invert the result afterwards const float shouldInvert = glm::dot( glm::vec3{inverseGrid[2]}, glm::cross(this->polygon[0] - this->polygon[1], this->polygon[2] - this->polygon[1])); using indextype = std::uint16_t; const std::vector<indextype> indices = mapbox::earcut<std::uint16_t>(polygons); MergedTriangles mergedTriangles = mergeTriangles(indices, this->polygon); for (Quadrilateral& quad : mergedTriangles.quadrilaterals) { if (shouldInvert < 0) { invert(quad); } 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()) { Triangle triangle{ Triangle{ .p1 = this->polygon[indices[i]], .p2 = this->polygon[indices[i + 1]], .p3 = this->polygon[indices[i + 2]], } }; if (shouldInvert < 0) { invert(triangle); } result.push_back(AppendToModel{ .newElement = Colored<Triangle>{ triangle, 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})); }