# HG changeset patch # User Teemu Piippo # Date 1655679891 -10800 # Node ID 8efa3a33172ebba6896d5d0e126e8d79f8961afd # Parent a9bf6bab5ea27eacb31286d132cae07a977db73d Add base code for circular primitives diff -r a9bf6bab5ea2 -r 8efa3a33172e CMakeLists.txt --- a/CMakeLists.txt Wed Jun 15 19:47:02 2022 +0300 +++ b/CMakeLists.txt Mon Jun 20 02:04:51 2022 +0300 @@ -47,6 +47,7 @@ src/settingseditor/settingseditor.cpp src/types/boundingbox.cpp # src/ui/canvas.cpp + src/ui/circletooloptions.cpp src/ui/multiplyfactordialog.cpp src/ui/objecteditor.cpp src/widgets/colorbutton.cpp @@ -58,6 +59,7 @@ ) set (LDFORGE_HEADERS src/basics.h + src/circularprimitive.h src/colors.h src/document.h src/documentmanager.h @@ -88,6 +90,7 @@ src/settingseditor/settingseditor.h src/types/boundingbox.h # src/ui/canvas.h + src/ui/circletooloptions.h src/ui/multiplyfactordialog.h src/ui/objecteditor.h src/widgets/colorbutton.h @@ -101,6 +104,7 @@ src/mainwindow.ui src/settingseditor/librarieseditor.ui src/settingseditor/settingseditor.ui + src/ui/circletool.ui src/ui/multiplyfactordialog.ui src/ui/objecteditor.ui src/widgets/colorselectdialog.ui @@ -117,6 +121,7 @@ set (LDFORGE_OTHER_FILES ) +set(CMAKE_AUTOUIC_SEARCH_PATHS src/ui) set(LDFORGE_RESOURCES ldforge.qrc) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) diff -r a9bf6bab5ea2 -r 8efa3a33172e icons/linetype-circularprimitive.png Binary file icons/linetype-circularprimitive.png has changed diff -r a9bf6bab5ea2 -r 8efa3a33172e icons/save-as-outline.png Binary file icons/save-as-outline.png has changed diff -r a9bf6bab5ea2 -r 8efa3a33172e icons/save-outline.png Binary file icons/save-outline.png has changed diff -r a9bf6bab5ea2 -r 8efa3a33172e icons_svg/linetype-circularprimitive.svg --- a/icons_svg/linetype-circularprimitive.svg Wed Jun 15 19:47:02 2022 +0300 +++ b/icons_svg/linetype-circularprimitive.svg Mon Jun 20 02:04:51 2022 +0300 @@ -1,59 +1,62 @@ + inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:dc="http://purl.org/dc/elements/1.1/"> + + + ionicons-v5-q + + + + id="metadata1900"> - image/svg+xml - ionicons-v5-q - - - ionicons-v5-q - diff -r a9bf6bab5ea2 -r 8efa3a33172e icons_svg/save-as-outline.svg --- a/icons_svg/save-as-outline.svg Wed Jun 15 19:47:02 2022 +0300 +++ b/icons_svg/save-as-outline.svg Mon Jun 20 02:04:51 2022 +0300 @@ -1,19 +1,19 @@ + inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:dc="http://purl.org/dc/elements/1.1/"> @@ -38,18 +38,26 @@ inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:window-width="1920" - inkscape:window-height="970" + inkscape:window-height="967" id="namedview3864" showgrid="false" - inkscape:zoom="0.27345145" - inkscape:cx="-30.25538" - inkscape:cy="-228.93654" + inkscape:zoom="0.7734375" + inkscape:cx="277.9798" + inkscape:cy="265.05051" inkscape:window-x="0" - inkscape:window-y="29" + inkscape:window-y="40" inkscape:window-maximized="1" - inkscape:current-layer="svg3862" /> + inkscape:current-layer="svg3862" + inkscape:pagecheckerboard="0" /> ionicons-v5-p + + inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:dc="http://purl.org/dc/elements/1.1/"> @@ -37,18 +37,26 @@ inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:window-width="1920" - inkscape:window-height="970" + inkscape:window-height="967" id="namedview3864" showgrid="false" - inkscape:zoom="0.27345145" - inkscape:cx="-30.25538" - inkscape:cy="-228.93654" + inkscape:zoom="1.546875" + inkscape:cx="146.10101" + inkscape:cy="10.343434" inkscape:window-x="0" - inkscape:window-y="29" + inkscape:window-y="40" inkscape:window-maximized="1" - inkscape:current-layer="svg3862" /> + inkscape:current-layer="svg3862" + inkscape:pagecheckerboard="0" /> ionicons-v5-p + +#include "basics.h" +#include "model.h" +#include "ldrawalgorithm.h" + +template +void rasterize(const CircularPrimitive& circ, Fn&& fn) +{ + std::vector result; + const auto xform = [&circ](const glm::vec2& p, float y){ + return glm::vec3{circ.transformation * glm::vec4{p.x, y, p.y, 1}}; + }; + const glm::vec3 origin = xform({0, 0}, 0); + switch(circ.type) { + case CircularPrimitive::Circle: + ldraw::circle(circ.fraction.segments, circ.fraction.divisions, [&] + (const glm::vec2&, const glm::vec2& p1, const glm::vec2& p2){ + fn(edge(xform(p1, 0), xform(p2, 0))); + }); + break; + case CircularPrimitive::Disc: + ldraw::circle(circ.fraction.segments, circ.fraction.divisions, [&] + (const glm::vec2&, const glm::vec2& p1, const glm::vec2& p2){ + fn(triangle(origin, xform(p1, 0), xform(p2, 0))); + }); + break; + case CircularPrimitive::Cylinder: + ldraw::circle(circ.fraction.segments, circ.fraction.divisions, [&] + (const glm::vec2&, const glm::vec2& p1, const glm::vec2& p2){ + fn(quadrilateral(xform(p1, 1), xform(p2, 1), xform(p2, 0), xform(p1, 0))); + }); + break; + case CircularPrimitive::CylinderOpen: + rasterize(CircularPrimitive{ + .type = CircularPrimitive::Cylinder, + .fraction = circ.fraction, + .transformation = circ.transformation, + }, fn); + rasterize(CircularPrimitive{ + .type = CircularPrimitive::Circle, + .fraction = circ.fraction, + .transformation = circ.transformation, + }, fn); + rasterize(CircularPrimitive{ + .type = CircularPrimitive::Circle, + .fraction = circ.fraction, + .transformation = glm::translate(circ.transformation, {0, 1, 0}), + }, fn); + break; + case CircularPrimitive::CylinderClosed: + rasterize(CircularPrimitive{ + .type = CircularPrimitive::CylinderOpen, + .fraction = circ.fraction, + .transformation = circ.transformation, + }, fn); + rasterize(CircularPrimitive{ + .type = CircularPrimitive::Disc, + .fraction = circ.fraction, + .transformation = circ.transformation, + }, fn); + break; + case CircularPrimitive::DiscNegative: + { + unsigned int i = 0; + ldraw::circle(circ.fraction.segments, circ.fraction.divisions, [&] + (const glm::vec2&, const glm::vec2& p1, const glm::vec2& p2){ + constexpr glm::vec2 corners[4] = { + {+1, +1}, + {-1, +1}, + {-1, -1}, + {+1, -1}, + }; + const glm::vec2& corner = corners[i * 4 / circ.fraction.divisions]; + fn(triangle(xform(p2, 0), xform(p1, 0), xform(corner, 0))); + ++i; + }); + } + break; + case CircularPrimitive::Chord: + for (unsigned int i = 1; i < circ.fraction.segments; ++i) { + const glm::vec2& p1 = ldraw::rimpoint(circ.fraction.divisions, i); + const glm::vec2& p2 = ldraw::rimpoint(circ.fraction.divisions, i + 1); + fn(triangle(xform(p2, 0), xform(p1, 0), xform({1, 0}, 0))); + } + break; + } +} diff -r a9bf6bab5ea2 -r 8efa3a33172e src/document.cpp --- a/src/document.cpp Wed Jun 15 19:47:02 2022 +0300 +++ b/src/document.cpp Mon Jun 20 02:04:51 2022 +0300 @@ -23,6 +23,7 @@ #include "model.h" #include "ui/objecteditor.h" #include "gl/partrenderer.h" +#include "circularprimitive.h" // Make mapbox::earcut work with glm::vec3 namespace mapbox { @@ -63,6 +64,11 @@ }); } +void EditTools::setCircleToolOptions(const CircleToolOptions& options) +{ + this->circleToolOptions = options; +} + void EditTools::mvpMatrixChanged(const glm::mat4& matrix) { this->mvpMatrix = matrix; @@ -138,19 +144,25 @@ painter->drawPolygon(QPolygonF{convertWorldPointsToScreenPoints(points, renderer)}); } -static opt> modelActionPoints(const ModelAction& action) +static std::vector> modelActionPoints(const ModelAction& action) { - opt> result; + 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 = {seg->p1, seg->p2}; + result.push_back({seg->p1, seg->p2}); } else if (const Triangle* tri = std::get_if>(&newElement)) { - result = {tri->p1, tri->p2, tri->p3}; + result.push_back({tri->p1, tri->p2, tri->p3}); } else if (const Quadrilateral* quad = std::get_if>(&newElement)) { - result = {quad->p1, quad->p2, quad->p3, quad->p4}; + 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; @@ -199,23 +211,36 @@ } } +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->actions()) { - const std::vector points = modelActionPoints(action).value_or(std::vector{}); - if (points.size() == 2) { - drawWorldPolyline(painter, points, renderer); - } - else { - if (worldPolygonWinding(points, this->renderer) == Winding::Clockwise) { - painter->setBrush(pens.greenPolygonBrush); + for (const ModelAction& action : this->modelActions()) { + for (const std::vector& points : modelActionPoints(action)) { + if (points.size() == 2) { + drawWorldPolyline(painter, points, renderer); } else { - painter->setBrush(pens.redPolygonBrush); + if (worldPolygonWinding(points, this->renderer) == Winding::Clockwise) { + painter->setBrush(pens.greenPolygonBrush); + } + else { + painter->setBrush(pens.redPolygonBrush); + } + drawWorldPolygon(painter, points, this->renderer); } - drawWorldPolygon(painter, points, this->renderer); } } painter->setBrush(pens.pointBrush); @@ -268,39 +293,21 @@ this->polygon.push_back(*this->worldPosition); } } - else if (true - and event->button() == Qt::RightButton - and this->polygon.size() > 1 - ) { - this->removeLastPoint(); + 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; } -} - -constexpr float distancesquared(const glm::vec3& p1, const glm::vec3& p2) -{ - const float dx = p2.x - p1.x; - const float dy = p2.y - p1.y; - const float dz = p2.z - p1.z; - return (dx * dx) + (dy * dy) + (dz * dz); -} - -inline float area(const Quadrilateral& q) -{ - return 0.5 * ( - glm::length(glm::cross(q.p2 - q.p1, q.p3 - q.p1)) + - glm::length(glm::cross(q.p3 - q.p2, q.p4 - q.p2)) - ); -} - -inline float energy(const Quadrilateral& q) -{ - const float L2 = distancesquared(q.p1, q.p2) - + distancesquared(q.p2, q.p3) - + distancesquared(q.p3, q.p4) - + distancesquared(q.p4, q.p1); - return 1 - 6.928203230275509 * area(q) / L2; + if (event->button() == Qt::RightButton and this->polygon.size() > 1) { + this->removeLastPoint(); + } } struct MergedTriangles @@ -365,7 +372,32 @@ return result; } -const std::vector EditTools::actions() const + +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) { @@ -415,7 +447,7 @@ void EditTools::closeShape() { - for (const ModelAction& action : this->actions()) { + for (const ModelAction& action : this->modelActions()) { Q_EMIT this->modelAction(action); } this->polygon.clear(); diff -r a9bf6bab5ea2 -r 8efa3a33172e src/document.h --- a/src/document.h Wed Jun 15 19:47:02 2022 +0300 +++ b/src/document.h Mon Jun 20 02:04:51 2022 +0300 @@ -27,7 +27,8 @@ enum EditingMode { SelectMode, - DrawMode + DrawMode, + CircleMode }; Q_DECLARE_METATYPE(EditingMode); @@ -56,6 +57,10 @@ glm::mat4 gridMatrix{1}; Plane gridPlane; opt worldPosition; + CircleToolOptions circleToolOptions = { + .fraction = {16, 16}, + .type = CircularPrimitive::Circle, + }; public: explicit EditTools(QObject *parent = nullptr); ~EditTools() override; @@ -64,6 +69,7 @@ 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); @@ -74,7 +80,9 @@ void mouseClick(const QMouseEvent* event) override; void overpaint(QPainter* painter) override; private: - const std::vector actions() const; + const std::vector modelActions() const; + const std::vector circleModeActions() const; + const std::vector drawModeActions() const; void closeShape(); void renderPreview(QPainter* painter, const void* pensptr); void removeLastPoint(); diff -r a9bf6bab5ea2 -r 8efa3a33172e src/gl/partrenderer.cpp --- a/src/gl/partrenderer.cpp Wed Jun 15 19:47:02 2022 +0300 +++ b/src/gl/partrenderer.cpp Mon Jun 20 02:04:51 2022 +0300 @@ -43,6 +43,7 @@ colorTable{colorTable} { this->setMouseTracking(true); + this->setFocusPolicy(Qt::WheelFocus); connect(model, &Model::rowsInserted, [&]{ this->needBuild = true; }); @@ -294,38 +295,42 @@ void PartRenderer::mouseMoveEvent(QMouseEvent* event) { - const bool left = event->buttons() & Qt::LeftButton; - const QPoint move = event->pos() - this->lastMousePosition; - this->totalMouseMove += move.manhattanLength(); - if (left and not move.isNull()) - { - // q_x is the rotation of the brick along the vertical y-axis, because turning the - // vertical axis causes horizontal (=x) rotation. Likewise q_y is the rotation of the - // brick along the horizontal x-axis, which causes vertical rotation. - const auto scalar = 0.006f; - const float move_x = static_cast(move.x()); - const float move_y = static_cast(move.y()); - const glm::quat q_x = glm::angleAxis(scalar * move_x, glm::vec3{0, -1, 0}); - const glm::quat q_y = glm::angleAxis(scalar * move_y, glm::vec3{-1, 0, 0}); - this->modelQuaternion = q_x * q_y * this->modelQuaternion; - this->updateModelMatrix(); + if (not this->frozen) { + const bool left = event->buttons() & Qt::LeftButton; + const QPoint move = event->pos() - this->lastMousePosition; + this->totalMouseMove += move.manhattanLength(); + if (left and not move.isNull()) + { + // q_x is the rotation of the brick along the vertical y-axis, because turning the + // vertical axis causes horizontal (=x) rotation. Likewise q_y is the rotation of the + // brick along the horizontal x-axis, which causes vertical rotation. + const auto scalar = 0.006f; + const float move_x = static_cast(move.x()); + const float move_y = static_cast(move.y()); + const glm::quat q_x = glm::angleAxis(scalar * move_x, glm::vec3{0, -1, 0}); + const glm::quat q_y = glm::angleAxis(scalar * move_y, glm::vec3{-1, 0, 0}); + this->modelQuaternion = q_x * q_y * this->modelQuaternion; + this->updateModelMatrix(); + } + this->lastMousePosition = event->pos(); + for (RenderLayer* layer : this->activeRenderLayers) { + layer->mouseMoved(event); + } + this->update(); } - this->lastMousePosition = event->pos(); - for (RenderLayer* layer : this->activeRenderLayers) { - layer->mouseMoved(event); - } - this->update(); } void PartRenderer::mousePressEvent(QMouseEvent* event) { - this->totalMouseMove = 0; - this->lastMousePosition = event->pos(); + if (not this->frozen) { + this->totalMouseMove = 0; + this->lastMousePosition = event->pos(); + } } void PartRenderer::mouseReleaseEvent(QMouseEvent* event) { - if (this->totalMouseMove < (2.0 / sqrt(2)) * 5.0) + if (not frozen and this->totalMouseMove < (2.0 / sqrt(2)) * 5.0) { for (RenderLayer* layer : this->activeRenderLayers) { layer->mouseClick(event); @@ -334,13 +339,22 @@ } } +void PartRenderer::keyReleaseEvent(QKeyEvent* event) +{ + if (event->key() == Qt::Key_Pause) { + this->frozen = not this->frozen; + } +} + void PartRenderer::wheelEvent(QWheelEvent* event) { - static constexpr double WHEEL_STEP = 1 / 1000.0; - const double move = (-event->angleDelta().y()) * WHEEL_STEP; - this->zoom = std::clamp(this->zoom + move, MIN_ZOOM, MAX_ZOOM); - this->updateViewMatrix(); - this->update(); + if (not this->frozen) { + static constexpr double WHEEL_STEP = 1 / 1000.0; + const double move = (-event->angleDelta().y()) * WHEEL_STEP; + this->zoom = std::clamp(this->zoom + move, MIN_ZOOM, MAX_ZOOM); + this->updateViewMatrix(); + this->update(); + } } void PartRenderer::addRenderLayer(RenderLayer* layer) diff -r a9bf6bab5ea2 -r 8efa3a33172e src/gl/partrenderer.h --- a/src/gl/partrenderer.h Wed Jun 15 19:47:02 2022 +0300 +++ b/src/gl/partrenderer.h Mon Jun 20 02:04:51 2022 +0300 @@ -28,6 +28,7 @@ bool needBuild = true; std::vector activeRenderLayers; std::vector inactiveRenderLayers; + bool frozen = false; public: PartRenderer( Model* model, @@ -56,6 +57,7 @@ void mouseMoveEvent(QMouseEvent* event) override; void mousePressEvent(QMouseEvent* event) override; void mouseReleaseEvent(QMouseEvent* event) override; + void keyReleaseEvent(QKeyEvent* event) override; void wheelEvent(QWheelEvent* event) override; Line<3> cameraLine(const QPointF& point) const; glm::vec3 unproject(const glm::vec3& win) const; diff -r a9bf6bab5ea2 -r 8efa3a33172e src/ldrawalgorithm.h --- a/src/ldrawalgorithm.h Wed Jun 15 19:47:02 2022 +0300 +++ b/src/ldrawalgorithm.h Mon Jun 20 02:04:51 2022 +0300 @@ -19,18 +19,45 @@ void makeUnofficial(ModelEditor &editor); */ + constexpr float circleAngle(int divisions, int i) + { + constexpr float ofs = 0.5 * pi<>; + float factor = -2.0f * pi<> / divisions; + return i * factor + ofs; + } + + constexpr glm::vec2 rimpoint(int divisions, int i) + { + const float angle = circleAngle(divisions, i); + return glm::vec2{std::sin(angle), std::cos(angle)}; + } + + template + void circleAngles(int segments, int divisions, Fn&& fn) + { + for (int i = 0; i < segments; i += 1) + { + const float a1 = circleAngle(divisions, i - 1); + const float a2 = circleAngle(divisions, i); + const float a3 = circleAngle(divisions, i + 1); + fn(a1, a2, a3); + } + } + template void circle(int segments, int divisions, Fn&& fn) { - float factor = 2.0f * pi<> / divisions; - for (int i = 0; i < segments; i += 1) - { + circleAngles(segments, divisions, [&fn]( + const float a1, + const float a2, + const float a3 + ){ fn( - glm::vec2{std::sin((i - 1) * factor), std::cos((i - 1) * factor)}, - glm::vec2{std::sin(i * factor), std::cos(i * factor)}, - glm::vec2{std::sin((i + 1) * factor), std::cos((i - 1) * factor)} + glm::vec2{std::sin(a1), std::cos(a1)}, + glm::vec2{std::sin(a2), std::cos(a2)}, + glm::vec2{std::sin(a3), std::cos(a3)} ); - } + }); } } diff -r a9bf6bab5ea2 -r 8efa3a33172e src/main.cpp --- a/src/main.cpp Wed Jun 15 19:47:02 2022 +0300 +++ b/src/main.cpp Mon Jun 20 02:04:51 2022 +0300 @@ -14,6 +14,7 @@ #include "settingseditor/settingseditor.h" #include "widgets/colorselectdialog.h" #include "settings.h" +#include "ui/circletooloptions.h" static const QDir LOCALE_DIR {":/locale"}; @@ -269,8 +270,9 @@ }; } -void initializeTools(Ui_MainWindow* ui, QWidget* parent) +void initializeTools(Ui_MainWindow* ui, DocumentManager* documents, QWidget* parent) { + CircleToolOptionsWidget* circleToolOptions = new CircleToolOptionsWidget{parent}; const struct { QString name, tooltip; @@ -289,6 +291,12 @@ .icon = {":/icons/pencil-outline.png"}, .widget = nullptr, }, + { + .name = QObject::tr("Circle"), + .tooltip = QObject::tr("Draw circular primitives."), + .icon = {":/icons/linetype-circularprimitive.png"}, + .widget = circleToolOptions, + }, }; for (int i = 0; i < countof(editingModesInfo); ++i) { const auto& editingModeInfo = editingModesInfo[i]; @@ -304,7 +312,18 @@ widget = new QWidget{parent}; } ui->toolWidgetStack->addWidget(widget); + QObject::connect(action, &QAction::triggered, [ui, i]{ + ui->toolWidgetStack->setCurrentIndex(i); + }); } + QObject::connect( + circleToolOptions, + &CircleToolOptionsWidget::optionsChanged, + [ui, documents](const CircleToolOptions& options) { + if (ModelData* data = currentModelData(ui, documents)) { + data->tools->setCircleToolOptions(options); + } + }); } constexpr bool sortModelIndexesByRow(const QModelIndex& a, const QModelIndex& b) @@ -583,7 +602,7 @@ action->setChecked(action->data().value() == mode); } }; - initializeTools(&ui, &mainWindow); + initializeTools(&ui, &documents, &mainWindow); for (QAction* action : ui.editingModesToolBar->actions()) { QObject::connect(action, &QAction::triggered, [&, action]{ if (ModelData* data = currentModelData(&ui, &documents)) { diff -r a9bf6bab5ea2 -r 8efa3a33172e src/model.cpp --- a/src/model.cpp Wed Jun 15 19:47:02 2022 +0300 +++ b/src/model.cpp Mon Jun 20 02:04:51 2022 +0300 @@ -19,6 +19,63 @@ #include #include "model.h" +constexpr unsigned int gcd(unsigned int a, unsigned int b) +{ + while (a != b) { + if (b > a) { + b -= a; + } + else if (a > b) { + a -= b; + } + } + return a; +} + +static_assert(gcd(16, 15) == 1); +static_assert(gcd(16, 4) == 4); +static_assert(gcd(272, 192) == 16); + +static constexpr const char* circularPrimitiveTypeString(const CircularPrimitive& circ) +{ + switch (circ.type) { + case CircularPrimitive::Circle: + return "edge"; + case CircularPrimitive::Disc: + return "disc"; + case CircularPrimitive::Cylinder: + return "cyli"; + case CircularPrimitive::CylinderOpen: + return "cylo"; + case CircularPrimitive::CylinderClosed: + return "cylc"; + case CircularPrimitive::DiscNegative: + return "ndis"; + case CircularPrimitive::Chord: + return "chrd"; + } + return ""; +} + +static QString circularPrimitiveFilePath(const CircularPrimitive& circ) +{ + QString result; + if (circ.fraction.divisions != 16) { + result += QString::number(circ.fraction.divisions) + QStringLiteral("\\"); + } + const int factor = gcd(circ.fraction.segments, circ.fraction.divisions); + int num = circ.fraction.segments / factor; + int denom = circ.fraction.divisions / factor; + if (denom < 4) { + num *= 4 / denom; + denom = 4; + } + result += QStringLiteral("%1-%2").arg(num).arg(denom); + result += QString::fromLatin1(circularPrimitiveTypeString(circ)); + result += QStringLiteral(".dat"); + return result; +} + static const char* iconPathForElement(const ModelElement& element) { return std::visit(overloaded{ @@ -37,6 +94,9 @@ [](const Colored&) { return ":/icons/linetype-conditionaledge.png"; }, + [](const Colored&) { + return ":/icons/linetype-circularprimitive.png"; + }, [](const Comment&) { return ":/icons/chatbubble-ellipses-outline.png"; }, @@ -102,6 +162,12 @@ .arg(vertexToString(cedge.c1)) .arg(vertexToString(cedge.c2)); }, + [](const Colored& circ) { + return QStringLiteral("1 %1 %2 %3") + .arg(circ.color.index) + .arg(transformToString(circ.transformation)) + .arg(circularPrimitiveFilePath(circ)); + }, [](const Comment& comment) { return "0 " + comment.text; }, diff -r a9bf6bab5ea2 -r 8efa3a33172e src/model.h --- a/src/model.h Wed Jun 15 19:47:02 2022 +0300 +++ b/src/model.h Mon Jun 20 02:04:51 2022 +0300 @@ -47,12 +47,50 @@ struct Empty {}; +struct CircularFraction +{ + unsigned int segments; + unsigned int divisions; +}; + +constexpr bool operator<(const CircularFraction& p, const CircularFraction& q) +{ + // a/b < c/d + // a < c * b / d + // a * d < c * b + return p.segments * q.divisions < q.segments / p.divisions; +} + +struct CircularPrimitive +{ + enum Type + { + Circle, + Disc, + Cylinder, + CylinderOpen, + CylinderClosed, + DiscNegative, + Chord, + } type; + static constexpr int NUM_TYPES = Chord + 1; + CircularFraction fraction; + glm::mat4 transformation; +}; + +struct CircleToolOptions +{ + CircularFraction fraction; + CircularPrimitive::Type type; +}; + using ModelElement = std::variant< Colored, Colored, Colored, Colored, Colored, + Colored, Comment, Empty, ParseError>; @@ -206,3 +244,22 @@ } } } + +constexpr Colored edge(const glm::vec3& p1, const glm::vec3& p2) +{ + return Colored{{.p1 = p1, .p2 = p2}, EDGE_COLOR}; +} + +constexpr Colored triangle(const glm::vec3& p1, const glm::vec3& p2, const glm::vec3& p3) +{ + return Colored{{.p1 = p1, .p2 = p2, .p3 = p3}, MAIN_COLOR}; +} + +constexpr Colored quadrilateral( + const glm::vec3& p1, + const glm::vec3& p2, + const glm::vec3& p3, + const glm::vec3& p4) +{ + return Colored{{.p1 = p1, .p2 = p2, .p3 = p3, .p4 = p4}, MAIN_COLOR}; +} diff -r a9bf6bab5ea2 -r 8efa3a33172e src/polygoncache.cpp --- a/src/polygoncache.cpp Wed Jun 15 19:47:02 2022 +0300 +++ b/src/polygoncache.cpp Mon Jun 20 02:04:51 2022 +0300 @@ -1,6 +1,7 @@ #include "polygoncache.h" #include "documentmanager.h" #include "invert.h" +#include "circularprimitive.h" Model* resolve(const QString& name, const ModelId callingModelId, DocumentManager* documents) { @@ -76,6 +77,26 @@ } } }, + [&result, id](const Colored& circ) { + rasterize(circ, [&](const ModelElement& element){ + std::visit(overloaded{ + // TODO: :-( + [&](const Colored& edge) { + result.push_back({{edge, edge.color}, id}); + }, + [&](const Colored& triangle) { + result.push_back({{triangle, triangle.color}, id}); + }, + [&](const Colored& quad) { + result.push_back({{quad, quad.color}, id}); + }, + [&](const Colored& cedge) { + result.push_back({{cedge, cedge.color}, id}); + }, + [&](const auto&){}, + }, element); + }); + }, [](const ModelElement&) {} }, element); } diff -r a9bf6bab5ea2 -r 8efa3a33172e src/ui/circletool.ui --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/ui/circletool.ui Mon Jun 20 02:04:51 2022 +0300 @@ -0,0 +1,98 @@ + + + CircleToolOptions + + + + 0 + 0 + 651 + 57 + + + + Form + + + + + + 1 + + + 16 + + + 16 + + + + + + + + + + + + + + true + + + 1 + + + QComboBox::NoInsert + + + + 8 + + + + + 16 + + + + + 48 + + + + + + + + + 64 + 0 + + + + 1 + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + diff -r a9bf6bab5ea2 -r 8efa3a33172e src/ui/objecteditor.cpp --- a/src/ui/objecteditor.cpp Wed Jun 15 19:47:02 2022 +0300 +++ b/src/ui/objecteditor.cpp Mon Jun 20 02:04:51 2022 +0300 @@ -12,7 +12,8 @@ const glm::vec3*, const glm::mat4*, const QString*, - ldraw::Color>; + ldraw::Color, + const CircularFraction*>; enum PropertyKey { @@ -27,6 +28,7 @@ Name, Text, Code, + Fraction, }; std::map getProperties(const ModelElement& element) @@ -63,6 +65,11 @@ result[Name] = &ref.name; result[Color] = ref.color; }, + [&](const Colored& circ) { + result[Transformation] = &circ.transformation; + result[Fraction] = &circ.fraction; + result[Color] = circ.color; + }, [&](Empty) {}, [&](const Comment& comment) { result[Text] = &comment.text;