Sun, 26 Jun 2022 21:00:06 +0300
Move render layer files to new src/layers/ directory
--- 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"