diff --git a/.gitignore b/.gitignore index 5ea6a7e..d19e35e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ # Don't track local Doxygen docs html/ latex/ +build*/ diff --git a/.travis.yml b/.travis.yml index c95562d..38de1a3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,16 +1,26 @@ +dist: xenial + sudo: false +git: + quiet: true + branches: only: - master addons: apt: + update: true packages: - doxygen + - qt5-default + - libeigen3-dev -script: - - doxygen Doxyfile +script: >- + qmake example/bezier_example.pro && + make && + doxygen Doxyfile deploy: provider: pages diff --git a/bezier.cpp b/BezierCpp/bezier.cpp similarity index 92% rename from bezier.cpp rename to BezierCpp/bezier.cpp index 4f54446..8694bc2 100644 --- a/bezier.cpp +++ b/BezierCpp/bezier.cpp @@ -17,6 +17,7 @@ void Curve::resetCache() cached_ext_points_.reset(); cached_bounding_box_tight_.reset(); cached_bounding_box_relaxed_.reset(); + cached_polyline_.reset(); } Curve::Coeffs Curve::bernsteinCoeffs() const @@ -132,6 +133,39 @@ PointVector Curve::getControlPoints() const return points; } +PointVector Curve::getPolyline(double smoothness) const +{ + if(!cached_polyline_ || smoothness != cached_polyline_smootheness_) + { + PointVector *polyline = new PointVector; + std::vector> subcurves; + subcurves.push_back(std::make_shared(this->getControlPoints())); + polyline->push_back(control_points_.row(0)); + while(!subcurves.empty()) + { + auto c = subcurves.back(); + subcurves.pop_back(); + auto cp = c->getControlPoints(); + double hull = 0; + for(int k = 1; k < cp.size(); k++) + hull += (cp.at(k-1) - cp.at(k)).norm(); + if (hull < smoothness * (cp.front() - cp.back()).norm()) + { + polyline->push_back(cp.back()); + } + else + { + auto split = c->splitCurve(); + subcurves.push_back(std::make_shared(split.second)); + subcurves.push_back(std::make_shared(split.first)); + } + } + (const_cast(this))->cached_polyline_smootheness_ = smoothness; + (const_cast(this))->cached_polyline_.reset(polyline); + } + return *cached_polyline_; +} + void Curve::manipulateControlPoint(int index, Point point) { control_points_.row(index) = point; @@ -140,10 +174,8 @@ void Curve::manipulateControlPoint(int index, Point point) void Curve::manipulateCurvature(double t, Point point) { - if (N_ > 4) - throw "Order of curve too big."; - if (N_ < 3) - throw "Not a curve."; + if (N_ < 3 || N_ > 4) + throw "Only quadratic and cubic curves can be manipulated"; double r = fabs((pow(t, N_ - 1) + pow(1 - t, N_ - 1) - 1) / (pow(t, N_ - 1) + pow(1 - t, N_ - 1))); double u = pow(1 - t, N_ - 1) / (pow(t, N_ - 1) + pow(1 - t, N_ - 1)); diff --git a/bezier.h b/BezierCpp/bezier.h similarity index 77% rename from bezier.h rename to BezierCpp/bezier.h index 074b339..20630ff 100644 --- a/bezier.h +++ b/BezierCpp/bezier.h @@ -23,16 +23,16 @@ #include /*! - * Nominal namespace + * Nominal namespace containing class definition and typedefs */ namespace Bezier { /*! - * \brief Point Point in xy plane + * \brief Point in xy plane */ typedef Eigen::Vector2d Point; /*! - * \brief Vector in xy plane + * \brief A Vector in xy plane */ typedef Eigen::Vector2d Vec2; /*! @@ -76,6 +76,8 @@ class Curve : public std::enable_shared_from_this cached_bounding_box_tight_; /*! If generated, stores bounding box (use_roots = true) for later use */ std::shared_ptr cached_bounding_box_relaxed_; /*! If generated, stores bounding box (use_roots = false) for later use */ + std::shared_ptr cached_polyline_; /*! If generated, stores polyline for later use */ + double cached_polyline_smootheness_ = 0; /*! Smootheness of cached polyline */ /// Reset all privately cached data inline void resetCache(); @@ -100,34 +102,41 @@ class Curve : public std::enable_shared_from_this public: /*! - * \brief Curve Create Bezier curve + * \brief Create the Bezier curve * \param points Nx2 matrix where each row is one of N control points that define the curve */ Curve(const Eigen::MatrixX2d& points); /*! - * \brief Curve Create Bezier curve + * \brief Create the Bezier curve * \param points A vector of control points that define the curve */ Curve(const PointVector& points); /*! - * \brief getControlPoints Get control points - * \return a Vector of control points + * \brief Get the control points + * \return A vector of control points */ PointVector getControlPoints() const; /*! - * \brief manipulateControlPoint Set new coordinates to a control point + * \brief Get a polyline representation of curve as a vector of points on curve + * \param smoothness smoothness factor > 1 (more resulting points when closer to 1) + * \return A vector of polyline vertices + */ + PointVector getPolyline(double smoothness = 1.001) const; + + /*! + * \brief Set the new coordinates to a control point * \param index Index of chosen control point * \param point New control point */ void manipulateControlPoint(int index, Point point); /*! - * \brief manipulateCurvature Manipulate curve so that it passes through wanted point for given 't' + * \brief Manipulate the curve so that it passes through wanted point for given 't' * \param t Curve parameter - * \param point Point where curve should pass for given t + * \param point Point where curve should pass for a given t * * Only works for quadratic and cubic curves * \warning Resets cached data @@ -135,7 +144,7 @@ class Curve : public std::enable_shared_from_this void manipulateCurvature(double t, Point point); /*! - * \brief elevateOrder Raise curve order by 1 + * \brief Raise the curve order by 1 * * Curve will always retain its shape * \warning Resets cached data @@ -143,7 +152,7 @@ class Curve : public std::enable_shared_from_this void elevateOrder(); /*! - * \brief lowerOrder Lower curve order by 1 + * \brief Lower the curve order by 1 * * If current shape cannot be described by lower order, it will be best aproximation * \warning Resets cached data @@ -151,43 +160,43 @@ class Curve : public std::enable_shared_from_this void lowerOrder(); /*! - * \brief valueAt Get point on curve for given t + * \brief Get the point on curve for a given t * \param t Curve parameter - * \return Point on a curve for given t + * \return Point on a curve for a given t */ Point valueAt(double t) const; /*! - * \brief curvatureAt Get curvature of curve for given t + * \brief Get curvature of curve for a given t * \param t Curve parameter - * \return Curvature of a curve for given t + * \return Curvature of a curve for a given t */ double curvatureAt(double t) const; /*! - * \brief tangentAt Get tangent of curve for given t + * \brief Get the tangent of curve for a given t * \param t Curve parameter - * \param normalize If resulting tangent should be normalized - * \return Tangent of a curve for given t + * \param normalize If the resulting tangent should be normalized + * \return Tangent of a curve for a given t */ Vec2 tangentAt(double t, bool normalize = true) const; /*! - * \brief normalAt Get normal of curve for given t + * \brief Get the normal of curve for a given t * \param t Curve parameter - * \param normalize If resulting normal should be normalized + * \param normalize If the resulting normal should be normalized * \return Normal of a curve for given t */ Vec2 normalAt(double t, bool normalize = true) const; /*! - * \brief getDerivative Get derivative of a curve + * \brief Get the derivative of a curve * \return Derivative curve */ Curve getDerivative() const; /*! - * \brief getRoots Get extreme points of curve + * \brief Get the extreme points of curve * \param step Size of step in coarse search * \param epsilon Precision of resulting t * \param max_iter Maximum number of iterations for Newton-Rhapson @@ -196,21 +205,21 @@ class Curve : public std::enable_shared_from_this std::vector getRoots(double step = 0.1, double epsilon = 0.001, std::size_t max_iter = 15) const; /*! - * \brief getBBox Get bounding box of curve + * \brief Get the bounding box of curve * \param use_roots If algorithm should use extreme points * \return Bounding box (if use_roots is false, returns the bounding box of control points) */ BBox getBBox(bool use_roots = true) const; /*! - * \brief splitCurve Split curve into two subcurves + * \brief Split the curve into two subcurves * \param z Parameter t at which to split the curve * \return Pair of two subcurves */ std::pair splitCurve(double z = 0.5) const; /*! - * \brief getPointsOfIntersection Get points of intersection with another curve + * \brief Get the points of intersection with another curve * \param curve Curve to intersect with * \param stop_at_first If first point of intersection is enough * \param epsilon Precision of resulting intersection @@ -220,7 +229,7 @@ class Curve : public std::enable_shared_from_this double epsilon = 0.001) const; /*! - * \brief projectPointOnCurve Get parameter t where curve is closes to given point + * \brief Get the parameter t where curve is closes to given point * \param point Point to project on curve * \param step Size of step in coarse search * \return Parameter t diff --git a/Doxyfile b/Doxyfile index 305421e..90641d8 100644 --- a/Doxyfile +++ b/Doxyfile @@ -781,7 +781,7 @@ WARN_LOGFILE = # spaces. See also FILE_PATTERNS and EXTENSION_MAPPING # Note: If this tag is empty the current directory is searched. -INPUT = . +INPUT = ./BezierCpp/ # This tag can be used to specify the character encoding of the source files # that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses diff --git a/README.md b/README.md index e3f22ba..87859c3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ # Bezier-Cpp +[![Build Status](https://travis-ci.com/stribor14/Bezier-cpp.svg?branch=master)](https://travis-ci.com/stribor14/Bezier-cpp) + Fast and lightweight class for using the Bezier curves of any order in C++ *Algorithm implementations are based on [A Primer on Bezier Curves](https://pomax.github.io/bezierinfo/) by Pomax* @@ -17,7 +19,7 @@ Fast and lightweight class for using the Bezier curves of any order in C++ - find points of intersection with another curve - elevate/lower order - manipulate control points - - manipulate dot on curve + - manipulate dot on curve (only for quadratic and cubic curves) ## Dependencies - c++11 @@ -25,5 +27,20 @@ Fast and lightweight class for using the Bezier curves of any order in C++ *Add compile flag* `-march=native` *when compiling to use vectorization with Eigen.* +## Example program +A small Qt5 based program written as a playground for manipulating Bezier curves. +### Usage + - starts with two Bezier curves (with 4 and 5 control points respectively) + - Zoom in/out: *__Ctrl + mouse wheel__* + - Manipulate control point or point on curve: *__Left mouse buttom__* + - Project mouse pointer on all curves and show tangent: *__Right mouse buttom__* + - Split curve at mouse point: *__Middle mouse buttom__* + - Raise order of the curve: *__Double left click__* + - Lower order of the curve *__Double right click__* + - Toggle bounding boxes and curve intersections: *__Double middle click__* + +### Additional dependencies + - qt5-default + ## Licence Apache License Version 2.0 diff --git a/example/bezier_example.pro b/example/bezier_example.pro new file mode 100644 index 0000000..07b42fe --- /dev/null +++ b/example/bezier_example.pro @@ -0,0 +1,48 @@ +#------------------------------------------------- +# +# Project created by QtCreator 2019-03-15T10:50:10 +# +#------------------------------------------------- + +QT += core gui + +greaterThan(QT_MAJOR_VERSION, 4): QT += widgets + +TARGET = bezier_example +TEMPLATE = app + +# The following define makes your compiler emit warnings if you use +# any feature of Qt which has been marked as deprecated (the exact warnings +# depend on your compiler). Please consult the documentation of the +# deprecated API in order to know how to port your code away from it. +DEFINES += QT_DEPRECATED_WARNINGS + +# You can also make your code fail to compile if you use deprecated APIs. +# In order to do so, uncomment the following line. +# You can also select to disable deprecated APIs only up to a certain version of Qt. +#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 + +CONFIG += c++11 +QMAKE_CXXFLAGS += -march=native + +INCLUDEPATH += /usr/include/eigen3 \ + ../BezierCpp + +SOURCES += \ + main.cpp \ + mainwindow.cpp \ + ../BezierCpp/bezier.cpp \ + qgraphicsviewzoom.cpp + +HEADERS += \ + mainwindow.h \ + ../BezierCpp/bezier.h \ + qgraphicsviewzoom.h + +FORMS += \ + mainwindow.ui + +# Default rules for deployment. +qnx: target.path = /tmp/$${TARGET}/bin +else: unix:!android: target.path = /opt/$${TARGET}/bin +!isEmpty(target.path): INSTALLS += target diff --git a/example/bezier_example.pro.user b/example/bezier_example.pro.user new file mode 100644 index 0000000..0099cea --- /dev/null +++ b/example/bezier_example.pro.user @@ -0,0 +1,325 @@ + + + + + + EnvironmentId + {ad48ea17-e69e-471d-ab8c-29488f8cc200} + + + ProjectExplorer.Project.ActiveTarget + 0 + + + ProjectExplorer.Project.EditorSettings + + true + false + true + + Cpp + + CppGlobal + + + + QmlJS + + QmlJSGlobal + + + 2 + UTF-8 + false + 4 + false + 80 + true + true + 1 + true + false + 0 + true + true + 0 + 8 + true + 1 + true + true + true + false + + + + ProjectExplorer.Project.PluginSettings + + + true + + + + ProjectExplorer.Project.Target.0 + + Desktop + Desktop + {4b429af8-c062-464b-bf8c-010130eb6e26} + 0 + 0 + 0 + + /home/mirko/Devel/Bezier-cpp/build-bezier_example-Desktop-Debug + + + true + qmake + + QtProjectManager.QMakeBuildStep + true + + false + false + false + + + true + Make + + Qt4ProjectManager.MakeStep + + false + + + false + + 2 + Build + + ProjectExplorer.BuildSteps.Build + + + + true + Make + + Qt4ProjectManager.MakeStep + + true + clean + + false + + 1 + Clean + + ProjectExplorer.BuildSteps.Clean + + 2 + false + + Debug + Debug + Qt4ProjectManager.Qt4BuildConfiguration + 2 + true + + + /home/mirko/Devel/Bezier-cpp/build-bezier_example-Desktop-Release + + + true + qmake + + QtProjectManager.QMakeBuildStep + false + + false + false + false + + + true + Make + + Qt4ProjectManager.MakeStep + + false + + + false + + 2 + Build + + ProjectExplorer.BuildSteps.Build + + + + true + Make + + Qt4ProjectManager.MakeStep + + true + clean + + false + + 1 + Clean + + ProjectExplorer.BuildSteps.Clean + + 2 + false + + Release + Release + Qt4ProjectManager.Qt4BuildConfiguration + 0 + true + + + /home/mirko/Devel/Bezier-cpp/build-bezier_example-Desktop-Profile + + + true + qmake + + QtProjectManager.QMakeBuildStep + true + + false + true + false + + + true + Make + + Qt4ProjectManager.MakeStep + + false + + + false + + 2 + Build + + ProjectExplorer.BuildSteps.Build + + + + true + Make + + Qt4ProjectManager.MakeStep + + true + clean + + false + + 1 + Clean + + ProjectExplorer.BuildSteps.Clean + + 2 + false + + Profile + Profile + Qt4ProjectManager.Qt4BuildConfiguration + 0 + true + + 3 + + + 0 + Deploy + + ProjectExplorer.BuildSteps.Deploy + + 1 + Deploy Configuration + + ProjectExplorer.DefaultDeployConfiguration + + 1 + + + false + false + 1000 + + true + + false + false + false + false + true + 0.01 + 10 + true + 1 + 25 + + 1 + true + false + true + valgrind + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + + 2 + + + Custom Executable + + ProjectExplorer.CustomExecutableRunConfiguration + + 3768 + false + true + false + false + true + + + + 1 + + + + ProjectExplorer.Project.TargetCount + 1 + + + ProjectExplorer.Project.Updater.FileVersion + 20 + + + Version + 20 + + diff --git a/example/main.cpp b/example/main.cpp new file mode 100644 index 0000000..07eec7b --- /dev/null +++ b/example/main.cpp @@ -0,0 +1,11 @@ +#include "mainwindow.h" +#include + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + MainWindow w; + w.show(); + + return a.exec(); +} diff --git a/example/mainwindow.cpp b/example/mainwindow.cpp new file mode 100644 index 0000000..331c256 --- /dev/null +++ b/example/mainwindow.cpp @@ -0,0 +1,66 @@ +#include "mainwindow.h" +#include "ui_mainwindow.h" + +#include + +MainWindow::MainWindow(QWidget *parent) : + QMainWindow(parent), + ui(new Ui::MainWindow), + scene(new CustomScene) +{ + ui->setupUi(this); + + ui->graphicsView->setScene(scene); + new QGraphicsViewZoom(ui->graphicsView); + + Eigen::MatrixX2d cp1, cp2; + +/* + cp1.resize(4,2); + cp2.resize(4,2); + cp1 << 10, 100, + 90, 30, + 164, 254, + 220, 220; + + cp2 << 101, 237, + 66, 121, + 217, 43,c1 = new qCurve(split.first); + 237, 182;/* + + cp1.resize(4,2); + cp2.resize(4,2); + cp1 << 34.8, 5.1, + 31.7, 6.2, + 31.1, 9.0, + 34.8, 10.0; + + cp2 << 33, 7.5, + 32.5, 8.4, + 31.2, 7.8, + 31.6, 6.8; +*/ + cp1.resize(4,2); + cp2.resize(5,2); + cp1 << 84, 162, + 246, 3, + 48, 236, + 180, 110; + + cp2 << 105, 79, + 275, 210, + 16, 48, + 164, 165, + 128, 128; + + scene->curves.push_back(new qCurve(cp1*5)); + scene->curves.push_back(new qCurve(cp2*5)); + + scene->addItem(scene->curves[0]); + scene->addItem(scene->curves[1]); +} + +MainWindow::~MainWindow() +{ + delete ui; +} diff --git a/example/mainwindow.h b/example/mainwindow.h new file mode 100644 index 0000000..62261f2 --- /dev/null +++ b/example/mainwindow.h @@ -0,0 +1,258 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include +#include +#include +#include +#include + +#include "bezier.h" +#include "qgraphicsviewzoom.h" + +namespace Ui { +class MainWindow; +} + +inline QTextStream& qStdOut() +{ + static QTextStream ts( stdout ); + return ts; +} + +class qCurve : public QGraphicsItem, public Bezier::Curve +{ +public: + qCurve(const Eigen::MatrixX2d &points) : QGraphicsItem(), Bezier::Curve(points){} + qCurve(const Bezier::Curve &curve) : QGraphicsItem(), Bezier::Curve(curve){} + qCurve(Bezier::Curve &&curve) : QGraphicsItem(), Bezier::Curve(curve){} + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) Q_DECL_OVERRIDE + { + painter->setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform, true); + + QPainterPath curve; + auto poly = getPolyline(); + curve.moveTo(poly[0].x(), poly[0].y()); + for(int k = 1; k < poly.size(); k++) + curve.lineTo(poly[k].x(), poly[k].y()); + painter->drawPath(curve); + + const int d = 6; + painter->setBrush(QBrush(Qt::blue, Qt::SolidPattern)); + Bezier::PointVector points = getControlPoints(); + for(int k = 1; k < points.size(); k++) + { + painter->setPen(Qt::blue); + painter->drawEllipse(points[k-1].x()-d/2, points[k-1].y()-d/2, d, d); + painter->setPen(QPen(QBrush(Qt::gray), 1, Qt::DotLine)); + painter->drawLine(points[k-1].x(), points[k-1].y(), points[k].x(), points[k].y()); + } + painter->setPen(Qt::blue); + painter->drawEllipse(points.back().x()-d/2, points.back().y()-d/2, d, d); + } + + QRectF boundingRect() const Q_DECL_OVERRIDE { + auto bbox = getBBox(false); + return QRectF(QPointF(bbox.min().x(), bbox.min().y()), QPointF(bbox.max().x(),bbox.max().y())); + } + +}; + +class CustomScene : public QGraphicsScene +{ +private: + // for projection + QGraphicsEllipseItem *dot; + QVector line; + QVector tan; + QVector boxes; + QVector dots; + bool draw_box_inter = false; + bool show_projection = false; + bool update_curvature = false; + std::pair t_to_update; + bool update_cp = false; + std::pair cp_to_update; +public: + QVector curves; + + void updateBoxIter(){ + for(auto &box: boxes) + removeItem(box); + for(auto &dot: dots) + removeItem(dot); + if (draw_box_inter){ + for(auto &curve: curves){ + auto bbox = curve->getBBox(true); + boxes.push_back(addRect(bbox.min().x(), bbox.min().y(), + bbox.max().x() - bbox.min().x(), bbox.max().y() - bbox.min().y(), + QPen(Qt::blue))); + } + for(int k = 0; k < curves.size() - 1; k++) + for(int i = k + 1; i < curves.size(); i++){ + auto inter = curves[k]->getPointsOfIntersection(*curves[i]); + for(auto &dot: inter) + dots.push_back(addEllipse(dot.x()-3, dot.y()-3, 6, 6, QPen(Qt::red), QBrush(Qt::red,Qt::SolidPattern))); + } + } + } + + void mousePressEvent(QGraphicsSceneMouseEvent *mouseEvent){ + const int sensitivity = 5; + Bezier::Point p(mouseEvent->scenePos().x(), mouseEvent->scenePos().y()); + if(mouseEvent->button() == Qt::RightButton){ + dot = addEllipse(QRectF(QPointF(p.x(), p.y()),QSizeF(6, 6)),QPen(Qt::yellow), QBrush(Qt::red,Qt::SolidPattern)); + for(auto curve : curves){ + auto t1 = curve->projectPointOnCurve(p); + auto p1 = curve->valueAt(t1); + auto tan1 = curve->tangentAt(t1); + line.push_back(addLine(QLineF(QPointF(p.x(),p.y()),QPointF(p1.x(),p1.y())),QPen(Qt::red))); + tan.push_back(addLine(QLineF(QPointF(p1.x(),p1.y()) - 150*QPointF(tan1.x(),tan1.y()), + QPointF(p1.x(),p1.y()) + 150*QPointF(tan1.x(),tan1.y())),QPen(Qt::blue))); + } + show_projection = true; + } + if(mouseEvent->button() == Qt::LeftButton){ + for(auto &curve: curves){ + auto pv = curve->getControlPoints(); + for(int k = 0; k < pv.size(); k++) + if ((pv[k] - p).norm() < sensitivity){ + update_cp = true; + cp_to_update = std::make_pair(curve, k); + } + if(update_cp) break; + double t = curve->projectPointOnCurve(p); + auto pt = curve->valueAt(t); + if((pt - p).norm() < 10){ + update_curvature = true; + t_to_update = std::make_pair(curve, t); + break; + } + } + } + if(mouseEvent->button() == Qt::MiddleButton){ + for(auto &curve: curves){ + auto t = curve->projectPointOnCurve(p); + if((curve->valueAt(t) - p).norm() < sensitivity){ + this->removeItem(curve); + qCurve *c1, *c2; + auto split = curve->splitCurve(t); + c1 = new qCurve(split.first); + c2 = new qCurve(split.second); + this->addItem(c1); + this->addItem(c2); + curves.push_back(c1); + curves.push_back(c2); + curves.removeOne(curve); + update(); + break; + } + } + } + QGraphicsScene::mousePressEvent(mouseEvent); + } + + void mouseMoveEvent(QGraphicsSceneMouseEvent *mouseEvent){ + Bezier::Point p(mouseEvent->scenePos().x(), mouseEvent->scenePos().y()); + if(show_projection){ + dot->setRect(QRectF(QPointF(p.x()-3,p.y()-3),QSizeF(6, 6))); + for(int k = 0; k < curves.size(); k++){ + auto t1 = curves[k]->projectPointOnCurve(Bezier::Point(p.x(), p.y())); + auto p1 = curves[k]->valueAt(t1); + auto tan1 = curves[k]->tangentAt(t1); + line[k]->setLine(QLineF(QPointF(p.x(),p.y()),QPointF(p1.x(),p1.y()))); + tan[k]->setLine(QLineF(QPointF(p1.x(),p1.y()) - 500*QPointF(tan1.x(),tan1.y()), + QPointF(p1.x(),p1.y()) + 500*QPointF(tan1.x(),tan1.y()))); + } + } + if(update_cp){ + cp_to_update.first->manipulateControlPoint(cp_to_update.second, p); + updateBoxIter(); + update(); + } + if(update_curvature){ + try{ + t_to_update.first->manipulateCurvature(t_to_update.second, p); + update(); + } + catch(char const* err) + { + update_curvature = false; + QMessageBox::warning(nullptr,"Warning", QString().sprintf("%s",err)); + } + updateBoxIter(); + } + QGraphicsScene::mouseMoveEvent(mouseEvent); + } + + void mouseReleaseEvent(QGraphicsSceneMouseEvent *mouseEvent){ + if(mouseEvent->button() == Qt::RightButton){ + if(show_projection){ + removeItem(dot); + for(int k = 0; k < curves.size(); k++){ + removeItem(line[k]); + removeItem(tan[k]); + } + line.clear(); + tan.clear(); + show_projection = false; + } + } + if(mouseEvent->button() == Qt::LeftButton){ + update_cp = false; + update_curvature = false; + } + QGraphicsScene::mouseReleaseEvent(mouseEvent); + } + + void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *mouseEvent){ + Bezier::Point p(mouseEvent->scenePos().x(), mouseEvent->scenePos().y()); + if(mouseEvent->button() == Qt::LeftButton) + for(auto &curve: curves){ + double t = curve->projectPointOnCurve(p); + auto pt = curve->valueAt(t); + if((pt - p).norm() < 10){ + curve->elevateOrder(); + } + } + if(mouseEvent->button() == Qt::RightButton) + for(auto &curve: curves){ + double t = curve->projectPointOnCurve(p); + auto pt = curve->valueAt(t); + if((pt - p).norm() < 10){ + try{ + curve->lowerOrder(); + } + catch(char const *err) + { + QMessageBox::warning(nullptr,"Warning", QString().sprintf("%s",err)); + } + } + } + if(mouseEvent->button() == Qt::MiddleButton){ + if(draw_box_inter) draw_box_inter = false; + else draw_box_inter = true; + } + updateBoxIter(); + update(); + QGraphicsScene::mouseDoubleClickEvent(mouseEvent); + } +}; + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = nullptr); + ~MainWindow(); + +private: + Ui::MainWindow *ui; + CustomScene *scene; +}; + + + + +#endif // MAINWINDOW_H diff --git a/example/mainwindow.ui b/example/mainwindow.ui new file mode 100644 index 0000000..11b97b9 --- /dev/null +++ b/example/mainwindow.ui @@ -0,0 +1,27 @@ + + + MainWindow + + + + 0 + 0 + 400 + 300 + + + + MainWindow + + + + + + + + + + + + + diff --git a/example/qgraphicsviewzoom.cpp b/example/qgraphicsviewzoom.cpp new file mode 100644 index 0000000..580ea13 --- /dev/null +++ b/example/qgraphicsviewzoom.cpp @@ -0,0 +1,60 @@ +#include "qgraphicsviewzoom.h" + +#include +#include +#include +#include + +QGraphicsViewZoom::QGraphicsViewZoom(QGraphicsView* view) + : QObject(view), m_view(view) +{ + m_view->viewport()->installEventFilter(this); + m_view->setMouseTracking(true); + m_modifiers = Qt::ControlModifier; + m_zoom_factor_base = 1.0005; +} + +void QGraphicsViewZoom::gentle_zoom(double factor) { + if (m_view->transform().m11() * factor < 1.0/30 ) return; // zoom x1/30 is minimum + if (m_view->transform().m11() * factor > 100) return; // zoom x100 is maximum + m_view->scale(factor, factor); + m_view->centerOn(target_scene_pos); + QPointF delta_viewport_pos = target_viewport_pos - QPointF(m_view->viewport()->width() / 2.0, + m_view->viewport()->height() / 2.0); + QPointF viewport_center = m_view->mapFromScene(target_scene_pos) - delta_viewport_pos; + m_view->centerOn(m_view->mapToScene(viewport_center.toPoint())); + + emit zoomed(); +} + +void QGraphicsViewZoom::set_modifiers(Qt::KeyboardModifiers modifiers) { + m_modifiers = modifiers; + +} + +void QGraphicsViewZoom::set_zoom_factor_base(double value) { + m_zoom_factor_base = value; +} + +bool QGraphicsViewZoom::eventFilter(QObject *object, QEvent *event) { + if (event->type() == QEvent::MouseMove) { + QMouseEvent* mouse_event = static_cast(event); + QPointF delta = target_viewport_pos - mouse_event->pos(); + if (qAbs(delta.x()) > 5 || qAbs(delta.y()) > 5) { + target_viewport_pos = mouse_event->pos(); + target_scene_pos = m_view->mapToScene(mouse_event->pos()); + } + } else if (event->type() == QEvent::Wheel) { + QWheelEvent* wheel_event = static_cast(event); + if (QApplication::keyboardModifiers() == m_modifiers) { + if (wheel_event->orientation() == Qt::Vertical) { + double angle = wheel_event->angleDelta().y(); + double factor = qPow(m_zoom_factor_base, angle); + gentle_zoom(factor); + return true; + } + } + } + Q_UNUSED(object) + return false; +} diff --git a/example/qgraphicsviewzoom.h b/example/qgraphicsviewzoom.h new file mode 100644 index 0000000..9404bca --- /dev/null +++ b/example/qgraphicsviewzoom.h @@ -0,0 +1,57 @@ +#ifndef QGRAPHICSVIEWZOOM_H +#define QGRAPHICSVIEWZOOM_H + +#include +#include +#include + +/*! + * \brief This class adds ability to zoom QGraphicsView using mouse wheel. The point under cursor + * remains motionless while it's possible. + * + * Note that it becomes not possible when the scene's + * size is not large enough comparing to the viewport size. QGraphicsView centers the picture + * when it's smaller than the view. And QGraphicsView's scrolls boundaries don't allow to + * put any picture point at any viewport position. + * + * When the user starts scrolling, this class remembers original scene position and + * keeps it until scrolling is completed. It's better than getting original scene position at + * each scrolling step because that approach leads to position errors due to before-mentioned + * positioning restrictions. + * + * When zommed using scroll, this class emits zoomed() signal. + * + * Usage: + * + * new Graphics_view_zoom(view); + * + * The object will be deleted automatically when the view is deleted. + * + * You can set keyboard modifiers used for zooming using set_modifiers(). Zooming will be + * performed only on exact match of modifiers combination. The default modifier is Ctrl. + * + * You can change zoom velocity by calling set_zoom_factor_base(). + * Zoom coefficient is calculated as zoom_factor_base^angle_delta + * (see QWheelEvent::angleDelta). + * The default zoom factor base is 1.0015. + */ +class QGraphicsViewZoom : public QObject { + Q_OBJECT +public: + QGraphicsViewZoom(QGraphicsView* view); + void gentle_zoom(double factor); + void set_modifiers(Qt::KeyboardModifiers modifiers); + void set_zoom_factor_base(double value); + +private: + QGraphicsView* m_view; + Qt::KeyboardModifiers m_modifiers; + double m_zoom_factor_base; + QPointF target_scene_pos, target_viewport_pos; + bool eventFilter(QObject* object, QEvent* event) Q_DECL_OVERRIDE; + +signals: + void zoomed(); +}; + +#endif // QGRAPHICSVIEWZOOM_H