
🫧 励志不掉头发的内向程序员 :个人主页
✨️ 个人专栏: 《C++语言》《Linux学习》
🌅偶尔悲伤,偶尔被幸福所完善
👓️博主简介:

文章目录
- 前言
- 一、对象捕捉是什么
- [二、关键类:RS_Snapper 与 RS_SnapMode](#二、关键类:RS_Snapper 与 RS_SnapMode)
-
- 2.1、RS_SnapMode:描述捕捉模式
- 2.2、RS_Snapper:执行捕捉
- [2.3、Action 天生具备捕捉能力](#2.3、Action 天生具备捕捉能力)
- [三、入口:Action 如何调用 snapPoint](#三、入口:Action 如何调用 snapPoint)
- [四、snapPoint 的内部流程](#四、snapPoint 的内部流程)
-
- [4.1、第一步:先把屏幕坐标转成 CAD 坐标](#4.1、第一步:先把屏幕坐标转成 CAD 坐标)
- 4.2、第二步:依次尝试各种捕捉
- 4.3、第三步:回退到自由捕捉
- 4.4、第四步:应用捕捉限制
- 五、端点捕捉怎么找到最近点
-
- [5.1、snapEndpoint 的实现](#5.1、snapEndpoint 的实现)
- 5.2、其他捕捉同理
- 六、从吸附点到实体创建
-
- [6.1、Snap 只修正坐标,不创建实体](#6.1、Snap 只修正坐标,不创建实体)
- 6.2、预览线同样使用吸附点
- 总结
前言
在上一篇文章中,我们讲清楚了 RS_GraphicView 如何通过 toGui 和 toGraph 完成 CAD 坐标与屏幕坐标之间的双向转换。鼠标点击窗口时,程序先经过 toGraph,把屏幕像素变成 CAD 坐标,然后交给当前 Action。
但这个坐标并不是最终使用的坐标。如果你用过 LibreCAD,一定注意过这种现象:当鼠标靠近某条直线的端点时,绘图点会被"吸"到端点上,而不是停留在鼠标正下方的位置。这正是 CAD 软件里非常重要的一环------对象捕捉(Snap)。
本篇文章就沿着这条链路继续往下走,回答三个问题:
- Snap 到底是什么,为什么要存在?
- 它由哪些类负责,内部如何组织?
- 鼠标坐标最终是怎样被修正成"吸附点"的?

一、对象捕捉是什么
1.1、一个直观的现象
画线时,鼠标可能停在 CAD 坐标 (10.02, 20.01) 上,但如果附近恰好有一条线段的端点在 (10, 20),LibreCAD 就会把绘图点修正到 (10, 20)。
从用户角度看,这像是"磁铁";从源码角度看,这是坐标被额外处理了一次。
1.2、为什么需要对象捕捉
原因很简单:人用鼠标不可能精确点中一个数学坐标。手抖、屏幕分辨率、缩放比例,都会带来微小误差。
而 CAD 追求的是精确绘图。所以需要一套机制,在鼠标坐标附近自动寻找"有意义的关键点",比如:
test
端点
中点
圆心
交点
线上最近点
网格点
1.3、核心问题
于是,画线流程里多了一步:
QMouseEvent 屏幕坐标 -> toGraph 转成 CAD 坐标 -> snapPoint 修正到最近关键点 -> 得到真正用于绘图的 RS_Vector
上一篇文章解决的是第一步 toGraph,本篇解决的就是第二步 snapPoint。
二、关键类:RS_Snapper 与 RS_SnapMode
LibreCAD 的对象捕捉主要由两个东西配合完成:RS_Snapper 负责"怎么找",RS_SnapMode 负责"找哪些"。
2.1、RS_SnapMode:描述捕捉模式
RS_SnapMode 是一个结构体,定义在 rs_snapper.h 里。它把各种捕捉类型用枚举列了出来:
cpp
struct RS_SnapMode {
enum SnapModes {
SnapIntersection = 1 << 0,
SnapOnEntity = 1 << 1,
SnapCenter = 1 << 2,
SnapDistance = 1 << 3,
SnapMiddle = 1 << 4,
SnapEndpoint = 1 << 5,
SnapGrid = 1 << 6,
SnapFree = 1 << 7,
RestrictHorizontal = 1 << 8,
RestrictVertical = 1 << 9,
RestrictOrthogonal = RestrictHorizontal | RestrictVertical,
SnapAngle = 1 << 10
};
...
};
同时,它还有一组 bool 开关,表示当前哪些捕捉被启用:
cpp
bool snapIntersection {false};
bool snapOnEntity {false};
bool snapCenter {false};
bool snapDistance {false};
bool snapMiddle {false};
bool snapEndpoint {false};
bool snapGrid {false};
bool snapFree {false};
bool snapAngle {false};
所以可以这样理解:
RS_SnapMode是一份"配置清单",决定本次捕捉要尝试哪些类型。
2.2、RS_Snapper:执行捕捉
真正干活的类是 RS_Snapper,同样定义在 rs_snapper.h 里。它的职责是:
根据当前的
RS_SnapMode,在鼠标坐标附近搜索最近的关键点,并返回修正后的坐标。
最核心的方法是 snapPoint:
cpp
RS_Vector snapPoint(const RS_Vector& coord, bool setSpot = false);
RS_Vector snapPoint(QMouseEvent* e);
其中接收 QMouseEvent* 的版本,就是鼠标事件进入后实际调用的入口。
2.3、Action 天生具备捕捉能力
关键点在 rs_actioninterface.h:
cpp
class RS_ActionInterface : public QObject, public RS_Snapper {
...
};
RS_ActionInterface 是所有绘图 Action 的基类,它同时继承了 RS_Snapper。这意味着:
每一个绘图 Action,天生就是一个"捕捉器"。
所以在 RS_ActionDrawLine 里,可以直接调用 snapPoint(e),不需要额外创建对象。
三、入口:Action 如何调用 snapPoint
我们直接看 RS_ActionDrawLine,文件是 rs_actiondrawline.cpp。
3.1、鼠标移动时
cpp
void RS_ActionDrawLine::mouseMoveEvent(QMouseEvent* e)
{
RS_Vector mouse = snapPoint(e);
if (getStatus() == SetEndpoint && pPoints->data.startpoint.valid) {
...
deletePreview();
RS_Line* line = new RS_Line(pPoints->data.startpoint, mouse);
preview->addEntity(line);
...
drawPreview();
}
}
鼠标移动时,mouse 已经是被捕捉修正过的点。预览线正是用这个修正后的点生成的,所以你会看到预览线"吸"在端点或中点上。
3.2、鼠标释放时
cpp
void RS_ActionDrawLine::mouseReleaseEvent(QMouseEvent* e)
{
if (e->button() == Qt::LeftButton) {
RS_Vector snapped = snapPoint(e);
...
RS_CoordinateEvent ce(snapped);
coordinateEvent(&ce);
}
...
}
鼠标释放时,同样先调用 snapPoint(e),得到最终吸附点,再把它封装成 RS_CoordinateEvent 交给状态机处理。
所以结论是:snapPoint 是鼠标事件进入绘图逻辑前的一道统一关卡。
四、snapPoint 的内部流程
snapPoint(QMouseEvent*) 是整篇文章的核心,实现位于 rs_snapper.cpp。
4.1、第一步:先把屏幕坐标转成 CAD 坐标
cpp
RS_Vector mouseCoord = graphicView->toGraph(e->x(), e->y());
这正是上一篇讲的 toGraph。捕捉的所有后续计算,都发生在这个 CAD 坐标附近。
4.2、第二步:依次尝试各种捕捉
cpp
double ds2Min = RS_MAXDOUBLE * RS_MAXDOUBLE;
if (snapMode.snapEndpoint) {
t = snapEndpoint(mouseCoord);
double ds2 = mouseCoord.squaredTo(t);
if (ds2 < ds2Min) {
ds2Min = ds2;
pImpData->snapSpot = t;
}
}
if (snapMode.snapCenter) {
t = snapCenter(mouseCoord);
...
}
if (snapMode.snapMiddle) {
t = snapMiddle(mouseCoord);
...
}
这里有几个值得注意的设计:
test
只有 snapMode 里对应的开关为 true 时,才会尝试这种捕捉。
每种捕捉都返回一个候选点 t。
用 ds2 = mouseCoord.squaredTo(t) 计算鼠标到候选点的平方距离。
用 ds2Min 记录目前找到的最小距离,只有更近的点才会被接受。
这样就实现了"多种捕捉同时开启时,自动选择离鼠标最近的那个"。
4.3、第三步:回退到自由捕捉
cpp
if (!pImpData->snapSpot.valid) {
pImpData->snapSpot = mouseCoord; // 默认回退到自由捕捉
}
如果所有捕捉都没有找到有效关键点,snapSpot 就是 false。这时程序直接使用鼠标原始坐标,也就是"自由捕捉"。
4.4、第四步:应用捕捉限制
在得到 snapSpot 之后,还可以再叠加正交、水平、垂直等限制:
cpp
switch (snapMode.restriction) {
case RS2::RestrictOrthogonal:
...
break;
case RS2::RestrictHorizontal:
...
break;
case RS2::RestrictVertical:
...
break;
default:
pImpData->snapCoord = pImpData->snapSpot;
break;
}
最终函数返回的是 pImpData->snapCoord。所以整个 snapPoint 的流程可以概括为:
toGraph 得到鼠标 CAD 坐标 -> 依次尝试启用的捕捉类型 -> 每种捕捉返回候选点 -> 用最小平方距离选出最近点 -> 没有有效点则回退自由捕捉 -> 应用正交/水平/垂直限制 -> 返回最终吸附点
五、端点捕捉怎么找到最近点
snapPoint 只是调度器,真正的几何搜索在下面一层。
5.1、snapEndpoint 的实现
cpp
RS_Vector RS_Snapper::snapEndpoint(const RS_Vector& coord) {
RS_Vector vec(false);
vec = container->getNearestEndpoint(coord, nullptr);
return vec;
}
它自己没有遍历实体,而是把工作交给了 container。这里的 container 就是当前图形容器 RS_Graphic。它会遍历所有实体,找到离 coord 最近的端点。
5.2、其他捕捉同理
不同捕捉类型,对应容器提供的不同搜索方法:
cpp
container->getNearestEndpoint(coord, ...) // 最近端点
container->getNearestCenter(coord, ...) // 最近圆心
container->getNearestMiddle(coord, ...) // 最近中点
container->getNearestPointOnEntity(coord, ...) // 线上最近点
这样,RS_Snapper 负责"决定要捕捉什么",RS_EntityContainer 负责"在图形数据里找出最近点",职责分得很清楚。
六、从吸附点到实体创建
6.1、Snap 只修正坐标,不创建实体
这一点很重要。snapPoint 返回的仍然只是一个 RS_Vector,它不会创建任何实体,也不会修改图形数据。真正的创建动作,仍然由 Action 完成。例如第二次点击后:
snapPoint 返回终点坐标 -> 封装成 RS_CoordinateEvent -> coordinateEvent 处理 -> new RS_Line(startPoint, endPoint) -> addEntity 加入 Graphic
6.2、预览线同样使用吸附点
在 mouseMoveEvent 里,预览线是这样创建的:
cpp
RS_Line* line = new RS_Line(pPoints->data.startpoint, mouse);
preview->addEntity(line);
这里的 mouse 已经经过 snapPoint 修正,所以预览线也会吸附到端点、中点等位置。用户移动鼠标时,就能直观地看到"接下来会画到哪里"。
总结
对象捕捉把"鼠标点到的位置"和"实际绘图的位置"区分开来,是 CAD 精确绘图的基础。
可以用一条链路概括:
QMouseEvent 屏幕坐标 -> toGraph 转成 CAD 坐标 -> RS_Snapper::snapPoint -> 按 RS_SnapMode 依次尝试端点/圆心/中点/交点等 -> 在容器中搜索最近关键点 -> 选距离最近者,必要时回退自由捕捉 -> 返回最终吸附点-> Action 用该点创建实体
这里有几个关键认知:
RS_SnapMode决定"找哪些",RS_Snapper决定"怎么找"。RS_ActionInterface继承RS_Snapper,所以每个 Action 都自带捕捉能力。snapPoint先用toGraph,再做几何搜索,最后返回修正坐标。- Snap 只修正坐标,不创建实体;创建仍然由 Action 完成。
🎇坚持到这里已经很厉害啦,辛苦啦🎇 ʕ • ᴥ • ʔ づ♡ど
