Sun, 09 Apr 2023 16:30:33 +0300
Also connect up the "Delete" action
/* * LDForge: LDraw parts authoring CAD * Copyright (C) 2013 - 2020 Teemu Piippo * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ #include <QMouseEvent> #include <QPainter> #include "src/model.h" #include "src/gl/partrenderer.h" #include "src/circularprimitive.h" #include "src/layers/edittools.h" #include "src/invert.h" #include "src/triangulate.h" EditTools::EditTools(QObject* parent) : QObject{parent}, RenderLayer{} { } EditTools::~EditTools() { } void EditTools::setEditMode(EditingMode newMode) { this->mode = newMode; switch (this->mode) { case SelectMode: Q_EMIT this->suggestCursor(Qt::ArrowCursor); break; case DrawMode: case CircleMode: Q_EMIT this->suggestCursor(Qt::CrossCursor); break; } } void EditTools::setGridMatrix(const glm::mat4& newGridMatrix) { this->gridMatrix = newGridMatrix; this->gridPlane = planeFromTriangle({ this->gridMatrix * glm::vec4{0, 0, 0, 1}, this->gridMatrix * glm::vec4{1, 0, 0, 1}, this->gridMatrix * glm::vec4{0, 1, 0, 1}, }); } void EditTools::setCircleToolOptions(const CircleToolOptions& options) { this->circleToolOptions = options; } void EditTools::mvpMatrixChanged(const glm::mat4& matrix) { this->mvpMatrix = matrix; } void EditTools::mouseMoved(const QMouseEvent* event) { this->worldPosition = this->renderer->screenToModelCoordinates(event->pos(), this->gridPlane); this->localPosition = event->localPos(); if (this->worldPosition.has_value()) { // Snap the position to grid. This procedure is basically the "change of basis" and almost follows the // A⁻¹ × M × A formula which is used to perform a transformation in some other coordinate system, except // we actually use the inverted matrix first and the regular one last to perform the transformation of // grid coordinates in our XY coordinate system. Also, we're rounding the coordinates which is obviously // not a linear transformation, but fits the pattern anyway. // First transform the coordinates to the XY plane... this->worldPosition = glm::inverse(this->gridMatrix) * glm::vec4{*this->worldPosition, 1}; // Then round the coordinates to integer precision... this->worldPosition = glm::round(*this->worldPosition); // And finally transform it back to grid coordinates by transforming it with the // grid matrix. this->worldPosition = this->gridMatrix * glm::vec4{*this->worldPosition, 1}; this->inputPolygon.updateCurrentPoint(*this->worldPosition); } } //! \brief Conversion function from PlainPolygonElement to ModelElement ModelElement elementFromPolygonAndColor(const PlainPolygonElement& poly, ColorIndex color) { // use std::visit with a templated lambda to resolve the type of poly. return std::visit([color](const auto& resolvedPoly) -> ModelElement { // unlike with normal templates we need to pry out the type out manually using PolygonType = std::decay_t<decltype(resolvedPoly)>; // add color and return as a model element. return Colored<PolygonType>{resolvedPoly, color}; }, poly); } static std::vector<std::vector<glm::vec3>> polygonsToBeInserted(const ModelAction& action) { std::vector<std::vector<glm::vec3>> result; if (const AppendToModel* append = std::get_if<AppendToModel>(&action)) { const ModelElement& newElement = append->newElement; if (const LineSegment* seg = std::get_if<Colored<LineSegment>>(&newElement)) { result.push_back({seg->p1, seg->p2}); } else if (const Triangle* tri = std::get_if<Colored<Triangle>>(&newElement)) { result.push_back({tri->p1, tri->p2, tri->p3}); } else if (const Quadrilateral* quad = std::get_if<Colored<Quadrilateral>>(&newElement)) { result.push_back({quad->p1, quad->p2, quad->p3, quad->p4}); } else if (const CircularPrimitive* circ = std::get_if<Colored<CircularPrimitive>>(&newElement)) { // rasterize the circle down to polygons, and append them to the result. rasterize(*circ, [&](const PlainPolygonElement& poly, const ColorIndex color){ AppendToModel append{elementFromPolygonAndColor(poly, color)}; const auto& subpoints = polygonsToBeInserted(append); std::copy(subpoints.begin(), subpoints.end(), std::back_inserter(result)); }); } } return result; } namespace { struct Pens { const QBrush pointBrush; const QPen pointPen; const QPen textPen; const QPen polygonPen; const QBrush greenPolygonBrush; const QBrush redPolygonBrush; }; } static const Pens brightPens{ .pointBrush = {Qt::black}, .pointPen = {QBrush{Qt::black}, 2.0}, .textPen = {Qt::black}, .polygonPen = {QBrush{Qt::black}, 2.0, Qt::DashLine}, .greenPolygonBrush = {QColor{64, 255, 128, 192}}, .redPolygonBrush = {QColor{255, 96, 96, 192}}, }; static const Pens darkPens{ .pointBrush = {Qt::white}, .pointPen = {QBrush{Qt::white}, 2.0}, .textPen = {Qt::white}, .polygonPen = {QBrush{Qt::white}, 2.0, Qt::DashLine}, .greenPolygonBrush = {QColor{64, 255, 128, 192}}, .redPolygonBrush = {QColor{255, 96, 96, 192}}, }; void EditTools::overpaint(QPainter* painter) { painter->save(); if (this->usePolygon()) { const Pens& pens = (this->renderer->isDark() ? darkPens : brightPens); this->renderPreview(painter, &pens); QFont font; font.setBold(true); if (this->usePolygon() and this->worldPosition.has_value()) { painter->setRenderHint(QPainter::Antialiasing); painter->setPen(pens.pointPen); painter->setBrush(pens.greenPolygonBrush); const QPointF pos = this->renderer->modelToScreenCoordinates(*this->worldPosition); painter->drawEllipse(pos, 5, 5); drawBorderedText(painter, pos + QPointF{5, 5}, font, vectorToString(*this->worldPosition)); } } painter->restore(); } const std::vector<ModelAction> EditTools::modelActions() const { switch(this->mode) { case SelectMode: return {}; case DrawMode: return drawModeActions(); case CircleMode: return circleModeActions(); } } void EditTools::renderPreview(QPainter* painter, const void* pensptr) { const Pens& pens = *reinterpret_cast<const Pens*>(pensptr); painter->setPen(pens.polygonPen); for (const ModelAction& action : this->modelActions()) { for (const std::vector<glm::vec3>& points : polygonsToBeInserted(action)) { if (points.size() == 2) { drawWorldPolyline(painter, points, renderer); } else { if (worldPolygonWinding(points, this->renderer) == Winding::Clockwise) { painter->setBrush(pens.greenPolygonBrush); } else { painter->setBrush(pens.redPolygonBrush); } drawWorldPolygon(painter, points, this->renderer); } } } painter->setBrush(pens.pointBrush); painter->setPen(pens.pointPen); for (const glm::vec3& point : this->inputPolygon) { drawWorldPoint(painter, point, this->renderer); } if (this->mode == CircleMode and this->inputPolygon.polygonSize() >= 2) { const glm::vec3 circleOrigin = this->inputPolygon[0]; const QPointF originScreen = this->renderer->modelToScreenCoordinates(circleOrigin); const auto extremity = [this, &originScreen](const glm::vec3& p){ const QPointF s2 = this->renderer->modelToScreenCoordinates(p); const auto intersection = rayRectangleIntersection( rayFromPoints(toVec2(originScreen), toVec2(s2)), this->renderer->rect()); if (intersection.has_value()) { return intersection->position; } else { return glm::vec2{s2.x(), s2.y()}; } }; const glm::vec3 zvec = this->gridMatrix[2]; if (this->inputPolygon.bufferSize() >= 3) { const glm::vec2 p1 = extremity(this->inputPolygon[0] + zvec); const glm::vec2 p2 = extremity(this->inputPolygon[0] - zvec); const glm::vec2 lateral = glm::normalize(glm::mat2{{0, 1}, {-1, 0}} * (p2 - p1)); painter->setPen(QPen{Qt::white, 3}); painter->drawLine(vecToQPoint(p1), vecToQPoint(p2)); constexpr float notchsize = 40.0f; for (int a = -30; a <= 30; ++a) { const glm::vec3 notch = this->inputPolygon[0] + static_cast<float>(a) * zvec; const QPointF s_notchcenter = this->renderer->modelToScreenCoordinates(notch); const QPointF notch_s1 = s_notchcenter + notchsize * 0.5f * vecToQPoint(lateral); const QPointF notch_s2 = s_notchcenter - notchsize * 0.5f * vecToQPoint(lateral); painter->drawLine(notch_s1, notch_s2); } const opt<float> height = this->cylinderHeight(); if (height.has_value()) { const glm::vec3 heightvec = height.value_or(0) * zvec; const glm::vec3 p = this->inputPolygon[1] + 0.5f * heightvec; QFont font{}; font.setBold(true); drawBorderedText(painter, this->renderer->modelToScreenCoordinates(p), font, QString::number(*height)); } } } } opt<float> EditTools::cylinderHeight() const { if (this->inputPolygon.bufferSize() < 3) { return {}; } else { const glm::vec3 cameravec = glm::normalize(this->renderer->cameraVector(this->localPosition)); const glm::vec3 heightvec = glm::normalize(glm::vec3{gridMatrix[2]}); const glm::vec3 normal = glm::cross(glm::cross(cameravec, heightvec), heightvec); const Plane plane{ .normal = normal, .anchor = this->inputPolygon[0], }; const opt<glm::vec3> p = this->renderer->screenToModelCoordinates(this->localPosition, plane); if (p.has_value()) { return std::round(glm::dot(*p - this->inputPolygon[0], heightvec)); } else { return {}; } } } EditingMode EditTools::currentEditingMode() const { return this->mode; } void EditTools::mouseClick(const QMouseEvent* event) { switch(this->mode) { case SelectMode: if (event->button() == Qt::LeftButton) { const std::int32_t highlighted = this->renderer->pick(event->pos()); Q_EMIT this->select({highlighted}, false); } break; case DrawMode: if (event->button() == Qt::LeftButton and this->worldPosition.has_value()) { if (this->inputPolygon.currentPointOnExistingPoint()) { this->closeShape(); } else { this->inputPolygon.finishCurrentPoint(); } } break; case CircleMode: if (event->button() == Qt::LeftButton) { if (this->inputPolygon.bufferSize() == 3) { this->closeShape(); } else if (this->worldPosition.has_value()) { this->inputPolygon.finishCurrentPoint(); } } break; } if (event->button() == Qt::RightButton) { this->inputPolygon.removeLastPoint(); } } const std::vector<ModelAction> EditTools::circleModeActions() const { std::vector<ModelAction> result; if (this->inputPolygon.polygonSize() >= 2) { const glm::vec3 x = this->inputPolygon[1] - this->inputPolygon[0]; const opt<float> cyliheight = this->cylinderHeight().value_or(1); glm::mat4 transform{ glm::vec4{x, 0}, *cyliheight * this->gridMatrix[2], glm::vec4{glm::cross(glm::vec3{-this->gridMatrix[2]}, x), 0}, glm::vec4{this->inputPolygon[0], 1}, }; Colored<CircularPrimitive> circ{ CircularPrimitive{ .type = this->circleToolOptions.type, .fraction = this->circleToolOptions.fraction, .transformation = transform, }, MAIN_COLOR }; result.push_back(AppendToModel{.newElement = circ}); } return result; } const std::vector<ModelAction> EditTools::drawModeActions() const { std::vector<ModelAction> result; if (this->inputPolygon.polygonSize() == 2) { result.push_back(AppendToModel{edge(this->inputPolygon[0], this->inputPolygon[1])}); } else if (this->inputPolygon.polygonSize() > 2) { for (const PlainPolygonElement& poly : polygonize( this->inputPolygon.begin(), this->inputPolygon.polygonEnd()) ) { result.push_back(AppendToModel{ .newElement = elementFromPolygonAndColor(poly, MAIN_COLOR), }); } } return result; } bool EditTools::usePolygon() const { switch (this->mode) { case SelectMode: return false; case DrawMode: case CircleMode: return true; } return {}; } void EditTools::closeShape() { for (const ModelAction& action : this->modelActions()) { Q_EMIT this->modelAction(action); } this->inputPolygon.clear(); }