下面是计算机图形学和计算机辅助设计大作业记录,实现的是线和圆弧的pick及snap功能,绘制使用的是QPainter,参考的AutoCAD的snap逻辑,原理是计算projectPoint, 即鼠标位置和curve的距离,当小于snapbox的尺寸时,修改snapbox的位置,操作时感觉靠近曲线磁吸过去了。
QXViewWgt .h
#pragma once
#include <QWidget>
#include "ui_QXViewWgt.h"
class QXViewWgt : public QWidget
{
Q_OBJECT
public:
QXViewWgt(QWidget *parent = Q_NULLPTR);
~QXViewWgt();
void paintEvent(QPaintEvent* event);
void mouseMoveEvent(QMouseEvent* event);
void mousePressEvent(QMouseEvent* event);
private:
Ui::QXViewWgt ui;
};
QXViewWgt .cpp
#include "QXViewWgt.h"
#include <QtGui/QPainter>
#include <QMouseEvent>
#include <Eigen/Eigen>
#include <vector>
#include <memory>
#define _USE_MATH_DEFINES
#include <math.h>
const double MIN_DIST = 1e-4;
namespace vege
{
static QPainter* s_painter = nullptr;
enum CurveState
{
CST_DEFAULT,
CST_SNAPPED,
CST_PICKED
};
class Curve2d
{
protected:
CurveState _state;
public:
Curve2d():_state(CST_DEFAULT)
{}
virtual ~Curve2d() {}
virtual bool projectPoint(Eigen::Vector2d p, Eigen::Vector2d& q) = 0;
virtual void paint() = 0;
virtual void setState(CurveState s)
{
if (_state == CST_PICKED && _state != s)
{
int k = 0;
}
_state = s;
};
virtual bool stateIs(CurveState s)
{
return _state == s;
};
};
class Line2d : public Curve2d
{
public:
Eigen::Vector2d _s;
Eigen::Vector2d _e;
Line2d(Eigen::Vector2d s, Eigen::Vector2d e):_s(s), _e(e)
{}
virtual bool projectPoint(Eigen::Vector2d p, Eigen::Vector2d& q)
{
Eigen::Vector2d d = _e - _s;
double v_se_length = d.norm();
d.normalize();
auto v_sp = p - _s;
auto v_sq_length = (p - _s).dot(d);
if (fabs(v_sq_length) < MIN_DIST)
{
q = _s;
return true;
}
if (fabs(v_sq_length -v_se_length) < MIN_DIST)
{
q = _e;
return true;
}
if (v_sq_length > 0.0 && v_sq_length < v_se_length)
{
q = _s + d * v_sq_length;
return true;
}
q = { 0, 0 };
return false;
}
virtual void paint()
{
switch (_state)
{
case CST_DEFAULT:
{
QPen pen0;
pen0.setColor(QColor(255, 255, 255));
pen0.setWidth(1);
s_painter->setPen(pen0);
s_painter->drawLine(_s(0), _s(1), _e(0), _e(1));
}
break;
case CST_SNAPPED:
{
QPen pen0;
pen0.setColor(QColor(255, 255, 255));
pen0.setWidth(4);
s_painter->setPen(pen0);
s_painter->drawLine(_s(0), _s(1), _e(0), _e(1));
pen0.setColor(QColor(0, 0, 0));
pen0.setWidth(2);
pen0.setStyle(Qt::DotLine);
s_painter->setPen(pen0);
s_painter->drawLine(_s(0), _s(1), _e(0), _e(1));
}
break;
case CST_PICKED:
{
QPen pen0;
pen0.setColor(QColor(255, 0, 255));
pen0.setWidth(2);
s_painter->setPen(pen0);
s_painter->drawLine(_s(0), _s(1), _e(0), _e(1));
}
break;
default:
break;
}
}
};
class Arc2d : public Curve2d
{
public:
Eigen::Vector2d _c;
double _r;
double _sang;
double _eang;
Arc2d(Eigen::Vector2d c, double r, double sang, double eang) :_c(c),
_r(r),
_sang(sang),
_eang(eang)
{}
double degToRad(double degv)
{
return (degv*M_PI) / 180.0;
}
double radToDeg(double radv)
{
return (radv*180.0) / M_PI;
}
Eigen::Vector2d startPoint()
{
return _c + Eigen::Vector2d(cos(degToRad(_sang)), sin(degToRad(_sang))) * _r;
}
Eigen::Vector2d endPoint()
{
return _c + Eigen::Vector2d(cos(degToRad(_eang)), sin(degToRad(_eang))) * _r;
}
double angle()
{
if (_eang > _sang)
{
return _eang - _sang;
}
else
{
return 360.0 - _sang + _eang;
}
}
virtual bool projectPoint(Eigen::Vector2d p, Eigen::Vector2d& q)
{
Eigen::Vector2d d = p - _c;
d.normalize();
Eigen::Vector2d s = startPoint();
if ((s - p).norm() < 1e-4)
{
q = s;
return true;
}
Eigen::Vector2d e = endPoint();
if ((e - p).norm() < 1e-4)
{
q = e;
return true;
}
bool on_flag = false;
double t = radToDeg(acos(d[0]));
if (d[1] < 0.0)
{
t = 360.0 - t;
if (t > 360.0)
{
t -= 360.0;
}
}
if (_eang > _sang)
{
if (t > _sang && t < _eang)
{
on_flag = true;
}
}
else
{
if ((t > _sang && t < 360.0) ||
(t >= 0.0 && t < _eang)
)
{
on_flag = true;
}
}
if (on_flag)
{
q = _c + d * _r;
return true;
}
q = { 0, 0 };
return false;
}
virtual void paint()
{
switch (_state)
{
case CST_DEFAULT:
{
QPen pen0;
pen0.setColor(QColor(255, 255, 255));
pen0.setWidth(1);
s_painter->setPen(pen0);
s_painter->drawArc(_c[0] - _r, _c[1] - _r, 2.0*_r, 2.0*_r, -_sang*16, -angle()*16);
}
break;
case CST_SNAPPED:
{
QPen pen0;
pen0.setColor(QColor(255, 255, 255));
pen0.setWidth(4);
s_painter->setPen(pen0);
s_painter->drawArc(_c[0] - _r, _c[1] - _r, 2.0*_r, 2.0*_r, -_sang * 16, -angle() * 16);
pen0.setColor(QColor(0, 0, 0));
pen0.setWidth(2);
pen0.setStyle(Qt::DotLine);
s_painter->setPen(pen0);
s_painter->drawArc(_c[0] - _r, _c[1] - _r, 2.0*_r, 2.0*_r, -_sang * 16, -angle() * 16);
}
break;
case CST_PICKED:
{
QPen pen0;
pen0.setColor(QColor(255, 0, 255));
pen0.setWidth(2);
s_painter->setPen(pen0);
s_painter->drawArc(_c[0] - _r, _c[1] - _r, 2.0*_r, 2.0*_r, -_sang * 16, -angle() * 16);
}
break;
default:
break;
}
}
};
}// end of namespace vege
Eigen::Vector2d _mosPos(0.0,0.0);
double _mosBoxHalfSize = 12;
double _snapSize = 1.414*_mosBoxHalfSize;
std::vector<vege::Curve2d*> _curves;
vege::Curve2d* _snappedCurve = nullptr;
vege::Curve2d* _pickedCurve = nullptr;
using namespace vege;
QXViewWgt::QXViewWgt(QWidget *parent)
: QWidget(parent)
{
ui.setupUi(this);
_curves.push_back(new Line2d({ 20,100 }, { 1200,100 }));
_curves.push_back(new Line2d({ 30,180 }, { 955,800 }));
_curves.push_back(new Arc2d({ 450,400 }, 200, 180, 45 ));
}
QXViewWgt::~QXViewWgt()
{
for (auto& curv : _curves)
{
delete curv;
}
}
void QXViewWgt::mousePressEvent(QMouseEvent* event)
{
if (_snappedCurve)
{
if (_pickedCurve == nullptr && _pickedCurve != _snappedCurve)
{
_pickedCurve = _snappedCurve;
_pickedCurve->setState(CST_PICKED);
}
}
else
{
if (_pickedCurve)
{
_pickedCurve->setState(CST_DEFAULT);
_pickedCurve = nullptr;
}
}
update();
}
void QXViewWgt::mouseMoveEvent(QMouseEvent* event)
{
_mosPos(0) = event->pos().x();
_mosPos(1) = event->pos().y();
Curve2d* curSnapped = nullptr;
for (auto& curve : _curves)
{
QPen pen0;
Eigen::Vector2d pt_q;
bool issucess = curve->projectPoint(_mosPos, pt_q);
if (issucess)
{
double dist = (pt_q - _mosPos).norm();
if (dist < _snapSize)
{
_mosPos = pt_q;
curSnapped = curve;
break;
}
}
}
if (curSnapped)
{
if (_pickedCurve && _pickedCurve == curSnapped)
{
_snappedCurve = curSnapped;
}
else
{
if (_snappedCurve)
{
if (_snappedCurve->stateIs(CST_SNAPPED))
{
_snappedCurve->setState(CST_DEFAULT);
}
}
_snappedCurve = curSnapped;
if (_snappedCurve->stateIs(CST_DEFAULT))
{
_snappedCurve->setState(CST_SNAPPED);
}
}
}
else
{
if (_snappedCurve)
{
if (_snappedCurve->stateIs(CST_SNAPPED))
{
_snappedCurve->setState(CST_DEFAULT);
}
_snappedCurve = nullptr;
}
}
this->update();
}
void QXViewWgt::paintEvent(QPaintEvent* event)
{
QPainter painter(this);
vege::s_painter = &painter;
painter.fillRect(0, 0, this->width(), this->height(), Qt::black);
// draw curves
for (auto& curve : _curves)
{
curve->paint();
}
// draw snap box
QPen pen;
pen.setColor(QColor(255, 255, 255));
pen.setWidth(2);
painter.setPen(pen);
painter.drawRect(_mosPos(0) - _mosBoxHalfSize, _mosPos(1) - _mosBoxHalfSize, _mosBoxHalfSize * 2, _mosBoxHalfSize * 2);
painter.end();
}
截图
1.绘制一些线和圆弧
2.当鼠标靠近线,线高亮,同时鼠标吸到线上,鼠标在snapbox范围轻微移动,将保持吸靠到线不变
3.此时单击线将被选中
4.单击空白处,将取消选中的线,整个操作逻辑符合AutoCAD的pick-snap风格
略