Mon, 20 Jun 2022 02:04:51 +0300
Add base code for circular primitives
--- 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)
--- 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 @@ <?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="512" height="512" viewBox="0 0 512 512" version="1.1" id="svg6" sodipodi:docname="linetype-circularprimitive.svg" - inkscape:version="1.0.2 (e86c870879, 2021-01-15)"> + 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/"> + <defs + id="defs10" /> + <sodipodi:namedview + id="namedview8" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + showgrid="false" + inkscape:zoom="1.0820313" + inkscape:cx="225.5018" + inkscape:cy="256.92419" + inkscape:window-width="1920" + inkscape:window-height="1047" + inkscape:window-x="1920" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="svg6" /> + <title + id="title2">ionicons-v5-q</title> + <path + style="fill:#ffff0a;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1" + d="m 242.42911,69.470162 -1.30366,19.592296 -85.52192,32.997432 -61.197671,80.83969 -4.516995,95.56456 39.263636,78.35865 90.59169,43.64258 L 324.15608,407.88121 398.94611,335.5168 417.7623,251.705 380.34973,157.84476 327.446,107.09009 259.81574,82.478128 278.92587,64.288364 435.88495,71.005695 447.27619,435.10552 66.095826,442.8476 70.646482,68.812661 Z" + id="path1444" /> + <path + d="M416,448H96a32.09,32.09,0,0,1-32-32V96A32.09,32.09,0,0,1,96,64H416a32.09,32.09,0,0,1,32,32V416A32.09,32.09,0,0,1,416,448Z" + style="fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px" + id="path4" /> + <circle + cx="253.52237" + cy="256.59106" + r="165.04449" + style="fill:none;stroke:#000000;stroke-width:27.5074px;stroke-linecap:round;stroke-linejoin:round" + id="circle4" /> <metadata - id="metadata12"> + id="metadata1900"> <rdf:RDF> <cc:Work rdf:about=""> - <dc:format>image/svg+xml</dc:format> - <dc:type - rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> <dc:title>ionicons-v5-q</dc:title> </cc:Work> </rdf:RDF> </metadata> - <defs - id="defs10" /> - <sodipodi:namedview - pagecolor="#ffffff" - bordercolor="#666666" - borderopacity="1" - objecttolerance="10" - gridtolerance="10" - guidetolerance="10" - inkscape:pageopacity="0" - inkscape:pageshadow="2" - inkscape:window-width="1920" - inkscape:window-height="962" - id="namedview8" - showgrid="false" - inkscape:zoom="0.39453125" - inkscape:cx="165.06309" - inkscape:cy="214.80274" - inkscape:window-x="0" - inkscape:window-y="29" - inkscape:window-maximized="1" - inkscape:current-layer="svg6" /> - <title - id="title2">ionicons-v5-q</title> - <circle - cx="256" - cy="256" - r="192" - style="fill:#ffff44;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px;fill-opacity:1" - id="circle4" /> </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 @@ <?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="512" height="512" viewBox="0 0 512 512" version="1.1" id="svg3862" sodipodi:docname="save-as-outline.svg" - inkscape:version="1.0.2 (e86c870879, 2021-01-15)"> + 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/"> <metadata id="metadata3868"> <rdf:RDF> @@ -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" /> <title id="title3858">ionicons-v5-p</title> + <rect + style="fill:#ffffff" + id="rect845" + width="224.55817" + height="91.655327" + x="93.8778" + y="100.38228" /> <path d="M380.93,57.37A32,32,0,0,0,358.3,48H94.22A46.21,46.21,0,0,0,48,94.22V417.78A46.21,46.21,0,0,0,94.22,464H417.78A46.36,46.36,0,0,0,464,417.78V153.7a32,32,0,0,0-9.37-22.63ZM256,416a64,64,0,1,1,64-64A63.92,63.92,0,0,1,256,416Zm48-224H112a16,16,0,0,1-16-16V112a16,16,0,0,1,16-16H304a16,16,0,0,1,16,16v64A16,16,0,0,1,304,192Z" style="fill:#ffa0a0;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px;fill-opacity:1"
--- a/icons_svg/save-outline.svg Wed Jun 15 19:47:02 2022 +0300 +++ b/icons_svg/save-outline.svg Mon Jun 20 02:04:51 2022 +0300 @@ -1,19 +1,19 @@ <?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="512" height="512" viewBox="0 0 512 512" version="1.1" id="svg3862" sodipodi:docname="save-outline.svg" - inkscape:version="1.0.2 (e86c870879, 2021-01-15)"> + 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/"> <metadata id="metadata3868"> <rdf:RDF> @@ -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" /> <title id="title3858">ionicons-v5-p</title> + <rect + style="fill:#ffffff" + id="rect845" + width="221.13829" + height="92.165718" + x="98.009506" + y="103.8269" /> <path d="M380.93,57.37A32,32,0,0,0,358.3,48H94.22A46.21,46.21,0,0,0,48,94.22V417.78A46.21,46.21,0,0,0,94.22,464H417.78A46.36,46.36,0,0,0,464,417.78V153.7a32,32,0,0,0-9.37-22.63ZM256,416a64,64,0,1,1,64-64A63.92,63.92,0,0,1,256,416Zm48-224H112a16,16,0,0,1-16-16V112a16,16,0,0,1,16-16H304a16,16,0,0,1,16,16v64A16,16,0,0,1,304,192Z" style="fill:#40a0ff;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px;fill-opacity:1"
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/circularprimitive.h Mon Jun 20 02:04:51 2022 +0300 @@ -0,0 +1,88 @@ +#pragma once +#include <glm/gtc/matrix_transform.hpp> +#include "basics.h" +#include "model.h" +#include "ldrawalgorithm.h" + +template<typename Fn> +void rasterize(const CircularPrimitive& circ, Fn&& fn) +{ + std::vector<ModelElement> 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; + } +}
--- 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<std::vector<glm::vec3>> modelActionPoints(const ModelAction& action) +static std::vector<std::vector<glm::vec3>> modelActionPoints(const ModelAction& action) { - opt<std::vector<glm::vec3>> result; + 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 = {seg->p1, seg->p2}; + result.push_back({seg->p1, seg->p2}); } else if (const Triangle* tri = std::get_if<Colored<Triangle>>(&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<Colored<Quadrilateral>>(&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<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; @@ -199,23 +211,36 @@ } } +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->actions()) { - const std::vector<glm::vec3> points = modelActionPoints(action).value_or(std::vector<glm::vec3>{}); - 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<glm::vec3>& 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<ModelAction> EditTools::actions() const + +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) { @@ -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();
--- 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<glm::vec3> 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<ModelAction> actions() const; + 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();
--- 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<float>(move.x()); - const float move_y = static_cast<float>(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<float>(move.x()); + const float move_y = static_cast<float>(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)
--- 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<RenderLayer*> activeRenderLayers; std::vector<RenderLayer*> 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;
--- 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<typename Fn> + 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<typename Fn> 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)} ); - } + }); } }
--- 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<EditingMode>() == 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)) {
--- 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 <QPixmap> #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<ConditionalEdge>&) { return ":/icons/linetype-conditionaledge.png"; }, + [](const Colored<CircularPrimitive>&) { + 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<CircularPrimitive>& 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; },
--- 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<SubfileReference>, Colored<LineSegment>, Colored<Triangle>, Colored<Quadrilateral>, Colored<ConditionalEdge>, + Colored<CircularPrimitive>, Comment, Empty, ParseError>; @@ -206,3 +244,22 @@ } } } + +constexpr Colored<LineSegment> edge(const glm::vec3& p1, const glm::vec3& p2) +{ + return Colored<LineSegment>{{.p1 = p1, .p2 = p2}, EDGE_COLOR}; +} + +constexpr Colored<Triangle> triangle(const glm::vec3& p1, const glm::vec3& p2, const glm::vec3& p3) +{ + return Colored<Triangle>{{.p1 = p1, .p2 = p2, .p3 = p3}, MAIN_COLOR}; +} + +constexpr Colored<Quadrilateral> quadrilateral( + const glm::vec3& p1, + const glm::vec3& p2, + const glm::vec3& p3, + const glm::vec3& p4) +{ + return Colored<Quadrilateral>{{.p1 = p1, .p2 = p2, .p3 = p3, .p4 = p4}, MAIN_COLOR}; +}
--- 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<CircularPrimitive>& circ) { + rasterize(circ, [&](const ModelElement& element){ + std::visit<void>(overloaded{ + // TODO: :-( + [&](const Colored<LineSegment>& edge) { + result.push_back({{edge, edge.color}, id}); + }, + [&](const Colored<Triangle>& triangle) { + result.push_back({{triangle, triangle.color}, id}); + }, + [&](const Colored<Quadrilateral>& quad) { + result.push_back({{quad, quad.color}, id}); + }, + [&](const Colored<ConditionalEdge>& cedge) { + result.push_back({{cedge, cedge.color}, id}); + }, + [&](const auto&){}, + }, element); + }); + }, [](const ModelElement&) {} }, element); }
--- /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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>CircleToolOptions</class> + <widget class="QWidget" name="CircleToolOptions"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>651</width> + <height>57</height> + </rect> + </property> + <property name="windowTitle"> + <string>Form</string> + </property> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QSpinBox" name="segments"> + <property name="minimum"> + <number>1</number> + </property> + <property name="maximum"> + <number>16</number> + </property> + <property name="value"> + <number>16</number> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>/</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="divisions"> + <property name="editable"> + <bool>true</bool> + </property> + <property name="currentIndex"> + <number>1</number> + </property> + <property name="insertPolicy"> + <enum>QComboBox::NoInsert</enum> + </property> + <item> + <property name="text"> + <string>8</string> + </property> + </item> + <item> + <property name="text"> + <string>16</string> + </property> + </item> + <item> + <property name="text"> + <string>48</string> + </property> + </item> + </widget> + </item> + <item> + <widget class="QLabel" name="ratio"> + <property name="minimumSize"> + <size> + <width>64</width> + <height>0</height> + </size> + </property> + <property name="text"> + <string>1</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="type"/> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui>
--- 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<PropertyKey, PropertyValue> getProperties(const ModelElement& element) @@ -63,6 +65,11 @@ result[Name] = &ref.name; result[Color] = ref.color; }, + [&](const Colored<CircularPrimitive>& circ) { + result[Transformation] = &circ.transformation; + result[Fraction] = &circ.fraction; + result[Color] = circ.color; + }, [&](Empty) {}, [&](const Comment& comment) { result[Text] = &comment.text;