【LibreCAD 2D架构】鼠标点下的坐标为什么会被“吸”走?LibreCAD 对象捕捉系统解析

🫧 励志不掉头发的内向程序员个人主页
✨️ 个人专栏: 《C++语言》《Linux学习》

🌅偶尔悲伤,偶尔被幸福所完善


👓️博主简介:

文章目录


前言

在上一篇文章中,我们讲清楚了 RS_GraphicView 如何通过 toGuitoGraph 完成 CAD 坐标与屏幕坐标之间的双向转换。鼠标点击窗口时,程序先经过 toGraph,把屏幕像素变成 CAD 坐标,然后交给当前 Action。

但这个坐标并不是最终使用的坐标。如果你用过 LibreCAD,一定注意过这种现象:当鼠标靠近某条直线的端点时,绘图点会被"吸"到端点上,而不是停留在鼠标正下方的位置。这正是 CAD 软件里非常重要的一环------对象捕捉(Snap)

本篇文章就沿着这条链路继续往下走,回答三个问题:

  1. Snap 到底是什么,为什么要存在?
  2. 它由哪些类负责,内部如何组织?
  3. 鼠标坐标最终是怎样被修正成"吸附点"的?

一、对象捕捉是什么

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 完成。


🎇坚持到这里已经很厉害啦,辛苦啦🎇 ʕ • ᴥ • ʔ づ♡ど

相关推荐
Sunsets_Red1 小时前
浅谈扫描线
c++·算法·编程·题解·洛谷·扫描线·信息学竞赛
明德扬1 小时前
AD9653采集模块怎样连接FPGA底板?从ADC采样到数据处理
学习·fpga开发·fpga
Generalzy1 小时前
像 gofmt 一样格式化 Python:Black、Ruff、YAPF、autopep8 谁才是 2026 年的首选?
开发语言·python
朝朝辞暮i1 小时前
C++第一课
开发语言·c++·算法
IT古董1 小时前
《FDE前沿部署工程师实战教程》11 - 企业Agent部署实战:Docker、API Gateway与生产环境
人工智能·学习
老赵的博客1 小时前
c++ tcp的三次握手
c++
李少兄1 小时前
JavaScript 对象完全指南
开发语言·javascript·ecmascript
张小姐的猫1 小时前
【AI大模型接入SDK】 —— Ollama本地接入Deepseek
java·linux·开发语言·网络·c++·人工智能
Nuanyt1 小时前
JUC常见核心知识梳理01 线程 并发 JMM volatile 管程 锁 synchronized
java·开发语言·网络·jvm