【LibreCAD 2D架构】从鼠标点击到图形创建:RS_ActionDrawLine交互流程与状态机解析
2026/9/7 21:10:27 网站建设 项目流程


🫧 励志不掉头发的内向程序员:个人主页

✨️ 个人专栏: 《C++语言》《Linux学习》
🌅偶尔悲伤,偶尔被幸福所完善


👓️博主简介:


文章目录

  • 前言
  • 一、RS_ActionDrawLine 如何处理第一次点击?
    • 1.1、RS_ActionDrawLine 等待什么?
    • 1.2、第一次点击从哪里进入 RS_ActionDrawLine?
    • 1.3、屏幕坐标不是 CAD 坐标
    • 1.4、Action 获取鼠标对应的 CAD 坐标
    • 1.5、第一次点击:记录起点
    • 1.6、第一次点击之后,Action 的状态发生了变化
    • 1.7、为什么第一次点击之后不会立即创建 Entity?
    • 1.8、鼠标移动为什么会出现预览线?
    • 1.9、把第一次点击完整串起来
    • 1.10、从源码阅读的角度,我们发现了什么?
  • 二、鼠标移动时发生什么?
    • 2.1、鼠标移动首先产生 `MouseMove` 事件
    • 2.2、第一次点击之后,Action 已经进入了新的阶段
    • 2.3、鼠标坐标还需要进行坐标转换
    • 2.4、为什么每移动一次都要重新计算?
    • 2.5、预览线并不等于真正的 `RS_Line`
    • 2.6、为什么预览线看起来像“实时绘制”?
    • 2.7、从源码角度应该关注什么?
    • 2.8、鼠标移动的完整流程
    • 2.9、到这里,画线过程已经越来越清晰
    • 2.10、下一步:第二次点击
  • 三、RS_ActionDrawLine 的状态机
    • 3.1、什么是状态机?
    • 3.2、RS_ActionDrawLine` 的核心状态
    • 3.3、第一次点击:状态发生第一次转换
    • 3.4、鼠标移动:状态不变,但数据不断变化
    • 3.5、第二次点击:状态机完成一次循环
    • 3.6、用状态转换图理解整个过程
    • 3.7、同一个事件,在不同状态下意义不同
    • 3.8、RS_ActionDrawLine` 为什么要使用状态机?
    • 3.9、从 `RS_ActionDrawLine` 看 LibreCAD 的 Action 设计
  • 总结

前言

上一篇文章中,我们追踪了 LibreCAD 的鼠标事件传递过程,了解了用户操作如何经过 Qt 的 QMouseEvent、绘图区、RS_EventHandler,最终进入当前工作的 RS_ActionDrawLine。但是,事件进入 RS_ActionDrawLine 之后,并不意味着一条直线就已经被创建了。对于用户来说,画线似乎只是简单地点击两下鼠标;而从源码角度来看,这两次点击之间还包含着坐标获取、状态保存、预览更新以及图形创建等多个步骤。例如,第一次点击时,程序需要确定直线的起点;鼠标继续移动时,程序需要不断获取当前鼠标位置,并显示跟随鼠标变化的临时预览线;第二次点击之后,程序才会真正确定终点,并创建最终的 RS_Line。因此,本篇文章将继续围绕 RS_ActionDrawLine 展开分析,重点研究三个问题:1、第一次鼠标点击后,RS_ActionDrawLine 如何获取并保存起点?2、鼠标移动时,为什么能够实时更新预览线?3、同样的鼠标点击,为什么在不同阶段会执行不同的操作?通过这几个问题,可以进一步理解 RS_ActionDrawLine 背后的状态机设计:
等待第一点 → 确定起点 → 等待第二点 → 更新预览 → 确定终点 → 创建 RS_Line。
也就是说,RS_ActionDrawLine 并不只是一个“画线函数”,而是一个负责管理完整用户交互过程的状态机。理解这一点,也就能更好地理解 LibreCAD 中其他 Action 的工作方式。


一、RS_ActionDrawLine 如何处理第一次点击?

前面我们已经追踪了鼠标事件的传递过程:
用户点击鼠标 -> QMouseEvent -> GraphicView -> RS_EventHandler -> 当前 Action -> RS_ActionDrawLine

现在,鼠标事件终于进入了真正负责“画线”的对象:RS_ActionDrawLine,那么问题来了当用户第一次点击绘图区时,RS_ActionDrawLine 到底做了什么?如果从用户的角度来看,这一步非常简单点击一下,确定直线的起点。但从源码角度来看,这次点击实际上完成了几个关键动作:
鼠标点击 -> 获取鼠标位置 -> 转换为 CAD 世界坐标 -> 记录起点 -> 进入下一阶段


1.1、RS_ActionDrawLine 等待什么?

在真正开始画线之前,RS_ActionDrawLine 并不知道用户想从哪里画。所以 Action 初始化之后,首先处于一种等待用户指定第一个点的状态。可以把它理解成 RS_ActionDrawLine 等待第一点。此时用户移动鼠标,并不会立即创建一条线。只有当用户第一次点击鼠标之后,Action 才获得了第一个确定的坐标。因此:

第一次点击 = 确定起点
第二次点击 = 确定终点

这实际上就是一个非常典型的状态驱动交互过程。


1.2、第一次点击从哪里进入 RS_ActionDrawLine?

上一节已经知道,鼠标事件最终会被 EventHandler 分发给当前 Action。因此,当用户点击鼠标左键时,最终会进入 RS_ActionDrawLine 对应的鼠标事件处理逻辑。这里可以重点关注 RS_ActionDrawLine 对鼠标事件的处理函数。

voidRS_ActionDrawLine::mouseMoveEvent(QMouseEvent*e){RS_Vector mouse=snapPoint(e);if(getStatus()==SetEndpoint&&pPoints->data.startpoint.valid){// Snapping to angle(15*) if shift key is pressedif(e->modifiers()&Qt::ShiftModifier){mouse=snapToAngle(mouse,pPoints->data.startpoint,15.);}deletePreview();RS_Line*line=newRS_Line(pPoints->data.startpoint,mouse);preview->addEntity(line);line->setLayerToActive();line->setPenToActive();drawPreview();}}voidRS_ActionDrawLine::mouseReleaseEvent(QMouseEvent*e){if(e->button()==Qt::LeftButton){RS_Vector snapped=snapPoint(e);// Snapping to angle(15*) if shift key is pressedif((e->modifiers()&Qt::ShiftModifier)&&getStatus()==SetEndpoint){snapped=snapToAngle(snapped,pPoints->data.startpoint,15.);}RS_CoordinateEventce(snapped);coordinateEvent(&ce);}elseif(e->button()==Qt::RightButton){deletePreview();switch(getStatus()){default:caseSetStartpoint:init(getStatus()-1);break;caseSetEndpoint:next();break;}}}

它拿到的并不是一个简单的:x、y。而是 Qt / LibreCAD 事件系统传递过来的鼠标事件信息。也就是说:

QMouseEvent -> 鼠标位置 -> Action

但这里还有一个非常重要的问题,鼠标位置究竟是屏幕坐标,还是 CAD 坐标?


1.3、屏幕坐标不是 CAD 坐标

假设用户在屏幕上点击:(500, 300),这个 (500, 300) 首先表示的是屏幕上的像素位置。但 CAD 中的直线并不是使用屏幕像素保存的。例如 CAD 图形可能使用:(125.35, 86.72)这样的世界坐标。所以第一次点击时,LibreCAD 不能简单地把鼠标位置 = 500,300 直接当成直线起点。中间还需要经过一个非常重要的步骤:屏幕坐标 -> CAD 世界坐标

RS_VectorRS_Snapper::snapPoint(QMouseEvent*e){// ....// 这一步就是将传过来的鼠标在屏幕上的坐标转换成世界坐标的函数RS_Vector mouseCoord=graphicView->toGraph(e->x(),e->y());// ....}

也就是我们后面要重点分析的坐标转换。


1.4、Action 获取鼠标对应的 CAD 坐标

当鼠标事件进入 Action 后,LibreCAD 会根据当前鼠标位置获取对应的 CAD 坐标。可以抽象成:
鼠标点击 -> 屏幕位置 -> GraphicView 坐标转换 -> CAD 世界坐标 -> RS_Vector

最终,Action 得到的是一个可以用于 CAD 几何计算的坐标。例如:

RS_Vector point;

可以把它理解成:

point(x,y);

例如:

point = (125.35, 86.72)

这才是后面创建 RS_Line 时真正使用的坐标。

RS_VectorRS_GraphicView::toGraph(intx,inty)const{returnRS_Vector(toGraphX(x),toGraphY(y));}// 鼠标在屏幕的位置 - 原点的位置 = x 的离原点的长度// x 离原点的长度 / 缩放比例 = 在图纸上的实际长度doubleRS_GraphicView::toGraphX(intx)const{return(x-offsetX)/factor.x;}// 这里屏幕和图纸的 y 轴是相反的,所以这里加了一个负号doubleRS_GraphicView::toGraphY(inty)const{return-(y-getHeight()+offsetY)/factor.y;}

1.5、第一次点击:记录起点

拿到第一个 CAD 坐标之后,RS_ActionDrawLine 就可以把它保存下来。逻辑上可以理解成:

第一次点击 -> 获取 CAD 坐标 -> startPoint = 当前坐标

也就是说:startPoint = point,此时并没有创建一条完整的线。因为现在只有起点。而一条直线至少需要起点和终点。所以第一次点击之后,LibreCAD 只是完成了 “我知道这条线从哪里开始了。”


1.6、第一次点击之后,Action 的状态发生了变化

第一次点击最重要的事情,其实不只是保存坐标。
它还意味着:

Action 从“等待第一点”进入了“等待第二点”的阶段。

enumStatus{SetStartpoint,///< Setting the startpointSetEndpoint///< Setting the endpoint};

整个过程可以理解为:

这就是 RS_ActionDrawLine 中非常重要的状态变化。


1.7、为什么第一次点击之后不会立即创建 Entity?

这一点非常容易产生误解。有人可能会认为第一次点击后就创建 RS_Line。实际上这时候还不能创建最终的直线。因为第一次点击只有起点,还缺少终点。所以第一次点击完成后,程序更像是在保存一个“中间状态”:

startPoint = 已确定
endPoint = 未确定

此时 Action 继续等待用户下一次操作。


1.8、鼠标移动为什么会出现预览线?

这时候就会出现一个非常有意思的现象。用户第一次点击之后,并没有马上点击第二次。但是当用户移动鼠标时,LibreCAD 的绘图区却可以看到一条跟随鼠标移动的预览线。
例如:

鼠标继续移动:

这说明第一次点击之后,Action 已经进入了一个新的状态:

起点已经确定
终点暂时使用鼠标当前位置

所以这里其实可以得到一个非常重要的结论:第一次点击并没有结束画线操作,而是把 RS_ActionDrawLine 从“等待起点”切换到了“等待终点”的交互阶段。

接下来鼠标移动事件就可以不断更新临时终点。


1.9、把第一次点击完整串起来

现在我们可以把第一次点击的完整流程总结成:
用户第一次点击 -> Qt 产生 QMouseEvent -> GraphicView 接收事件 -> RS_EventHandler 分发事件 -> RS_ActionDrawLine -> 获取鼠标位置 -> 屏幕坐标 -> CAD 世界坐标 -> 保存起点 -> 状态发生变化 -> 等待第二点

用一句话概括就是:第一次点击的本质,是让 RS_ActionDrawLine 获取并保存用户指定的第一个 CAD 坐标,同时将 Action 从“等待起点”切换到“等待终点”的状态。


1.10、从源码阅读的角度,我们发现了什么?

到这里,我们实际上已经开始看到 LibreCAD 的一个重要设计特点 RS_ActionDrawLine 并不是点击一次后直接画线,而是维护了一个交互过程:
Action -> 等待输入 -> 获取第一点 -> 保存状态 -> 等待下一次输入 -> 获取第二点 -> 创建 Entity

所以,LibreCAD 中的 Action 更像是一个小型状态机。这也是为什么前面我们一直强调:

EventHandler -> 负责把事件交给 Action
Action -> 负责根据当前状态解释事件

同样一个 MouseButtonPress,在不同 Action、不同状态下,产生的结果完全不同。对于 RS_ActionDrawLine:

第一次点击 → 设置起点
第二次点击 → 设置终点并创建直线

而对于其他 Action,RS_ActionDrawCircle

第一次点击 → 设置圆心
第二次操作 → 确定半径

因此:EventHandler 解决的是“事件交给谁”,而 Action 解决的是“这个事件在当前状态下意味着什么”。

接下来我们还剩下一个非常关键的问题:第一次点击之后,用户移动鼠标时,RS_ActionDrawLine 是如何不断更新终点并显示预览线的?这就是下一节要分析的内容。



二、鼠标移动时发生什么?

上一节我们分析了RS_ActionDrawLine如何处理第一次鼠标点击。第一次点击之后,LibreCAD 已经获得了直线的起点:

但是此时终点还没有确定。如果用户继续移动鼠标,会发现一个非常有意思的现象:

鼠标移动到另一个位置:

线段会跟着鼠标一起移动。那么问题来了:鼠标只是移动了一下,LibreCAD 为什么就能实时更新这条线?
答案就是:

鼠标移动会不断产生MouseMove事件,而RS_ActionDrawLine会利用这些事件不断更新当前的鼠标坐标,并重新绘制预览图形。


2.1、鼠标移动首先产生MouseMove事件

和鼠标点击一样,鼠标移动也属于 Qt 的事件。当用户移动鼠标时,Qt 会产生:

MouseMove -> QMouseEvent

然后按照前面分析的事件传递流程进入 LibreCAD:

用户移动鼠标 -> Qt -> QMouseEvent -> GraphicView -> EventHandler -> 当前 Action -> RS_ActionDrawLine

所以,如果用户快速移动鼠标,实际上会产生大量连续的鼠标移动事件。可以理解为:

**鼠标移动 -> MouseMove -> 鼠标移动 -> MouseMove -> 鼠标移动 -> MouseMove -> ……**

LibreCAD 正是通过不断处理这些事件,才能让绘图过程看起来是实时的。

voidRS_ActionDrawLine::mouseMoveEvent(QMouseEvent*e){RS_Vector mouse=snapPoint(e);if(getStatus()==SetEndpoint&&pPoints->data.startpoint.valid){// Snapping to angle(15*) if shift key is pressedif(e->modifiers()&Qt::ShiftModifier){mouse=snapToAngle(mouse,pPoints->data.startpoint,15.);}deletePreview();RS_Line*line=newRS_Line(pPoints->data.startpoint,mouse);preview->addEntity(line);line->setLayerToActive();line->setPenToActive();drawPreview();}}

2.2、第一次点击之后,Action 已经进入了新的阶段

这里是理解整个过程的关键。第一次点击之前 RS_ActionDrawLine 等待起点。第一次点击之后:

RS_ActionDrawLine 起点 = 已确定 终点 = 未确定

因此,当MouseMove事件到来时,Action 已经知道:

起点在哪里。

但还不知道:

终点在哪里。

所以当前鼠标位置就可以暂时充当“临时终点”。

例如:

鼠标移动之后:

因此可以简单理解为:

startPoint = 已确定的起点 currentPoint = 当前鼠标位置

预览线就是:

startPoint → currentPoint

2.3、鼠标坐标还需要进行坐标转换

这里又会遇到前面提到的一个问题。Qt 的QMouseEvent给出的鼠标位置,本质上是窗口/绘图区中的屏幕坐标。例如:

鼠标位置: (600, 350)

但是 CAD 图形使用的是自己的世界坐标。所以 LibreCAD 不能简单地:

endPoint=mouseX,mouseY;

而需要将鼠标所在的屏幕位置转换成 CAD 坐标。整体过程可以表示成:

QMouseEvent -> 鼠标屏幕坐标 -> GraphicView 坐标转换 -> CAD 世界坐标 -> currentPoint

例如:

屏幕坐标 坐标转换 CAD 坐标 (600, 350) ————————> (125.4, 86.7)

这样RS_ActionDrawLine才能使用这个坐标进行 CAD 几何计算。


2.4、为什么每移动一次都要重新计算?

假设第一次点击确定:

startPoint = (100, 100)

此时鼠标位于:

currentPoint = (200, 150)

那么预览线就是:

如果鼠标继续移动:

currentPoint = (300,200)

那么预览线就应该变成:

所以每一次MouseMove,都需要重新获取:

当前鼠标位置

然后更新:

currentPoint

再根据:

startPoint → currentPoint

重新计算预览结果。这就是为什么鼠标移动时,预览线能够实时跟随鼠标。

2.5、预览线并不等于真正的RS_Line

这里有一个非常重要的区别。第一次点击之后,屏幕上虽然已经能够看到一条线:但这并不意味着 LibreCAD 已经创建了一条最终的RS_Line。因为现在:

起点 = 确定
终点 = 未确定

所以这条线实际上只是:

绘图过程中的临时预览。

可以把它分成两个概念:

预览阶段 startPoint ───── currentPoint(鼠标位置)

和最终确定:

最终结果 startPoint ───── endPoint

只有用户第二次点击之后,终点才真正确定,LibreCAD 才会进入创建实体的流程。因此:

鼠标移动阶段主要是在更新“临时状态”,而不是不断创建新的 CAD 实体。

这一点非常重要。


2.6、为什么预览线看起来像“实时绘制”?

实际上,LibreCAD 并不是:

鼠标移动 -> 创建一条新的 RS_Line -> 鼠标再移动 -> 删除上一条 RS_Line -> 创建下一条 RS_Line

如果这样做,效率会非常低,而且会污染真正的 CAD 文档。更合理的方式是:

鼠标移动 -> 更新当前 Action 的临时数据 -> 重新绘制预览

也就是说:

最终用户第二次点击之后,临时状态才会转化成真正的图形实体。


2.7、从源码角度应该关注什么?

如果继续往RS_ActionDrawLine源码里追踪,这一阶段最值得关注的不是“画线函数叫什么”,而是三个问题:

第一,鼠标移动事件进入了哪个函数?

也就是:

mouseMoveEvent(...)

或者当前 LibreCAD 版本中对应的鼠标移动处理函数。


第二,鼠标当前位置保存到了哪里?

重点寻找类似:

mousePoint currentPoint coord snapPoint

这样的数据。


第三,鼠标移动之后为什么会重新显示?

也就是继续追踪:

MouseMove -> 更新坐标 -> 更新 Action 状态 -> 请求重新绘制 -> GraphicView 重绘

这一条链实际上已经开始涉及 LibreCAD 的预览绘制机制

2.8、鼠标移动的完整流程

现在可以把整个过程总结起来:

第一次点击 -> 确定 startPoint -> 进入“等待终点”状态 -> 用户移动鼠标 -> Qt 产生 MouseMove -> QMouseEvent -> GraphicView -> EventHandler -> RS_ActionDrawLine -> 获取当前鼠标位置 -> 屏幕坐标 -> CAD 世界坐标 -> 更新 currentPoint -> 根据 startPoint + currentPoint计算临时预览线 -> 重新绘制

所以用户看到的:

“鼠标一动,线就跟着动。”

背后实际上是一条不断重复的事件处理流程:

MouseMove -> 获取坐标 -> 更新状态 -> 重新绘制 -> MouseMove -> 获取坐标 -> 更新状态 -> 重新绘制 -> ……

2.9、到这里,画线过程已经越来越清晰

现在把前三个阶段放在一起:

  1. 点击“画线”:
    QAction -> ActionHandler -> RS_ActionDrawLine
  2. 第一次点击:
    QMouseEvent -> EventHandler -> RS_ActionDrawLine -> 确定起点
  3. 鼠标移动:
    MouseMove -> EventHandler -> RS_ActionDrawLine -> 获取当前坐标 -> 更新临时终点 -> 绘制预览

此时屏幕上的线虽然已经出现,但它还不是最终的 CAD 图形。真正决定这条线的是下一次鼠标点击。


2.10、下一步:第二次点击

当用户再次按下鼠标左键:

第二次点击 -> 确定终点 -> startPoint + endPoint -> 创建 RS_Line -> 加入 Graphic / Document -> 最终显示在绘图区

至此,“画线”这个交互过程才真正完成。因此,整个RS_ActionDrawLine的核心流程可以概括成:

等待第一点 -> 第一次点击 -> 确定起点 -> 鼠标移动 -> 实时更新临时终点 -> 显示预览 -> 第二次点击 -> 确定终点 -> 创建 RS_Line

下一节,我们就可以继续追踪最后一步:第二次点击之后,RS_ActionDrawLine如何把两个点变成真正的RS_Line


三、RS_ActionDrawLine 的状态机

前面我们分别分析了:

  • 用户第一次点击如何确定起点;
  • 鼠标移动时如何更新预览;
  • 第二次点击如何确定终点。

如果把这些过程拆开来看,会感觉只是几个独立的鼠标事件。但实际上,RS_ActionDrawLine并不是简单地:

收到一次鼠标事件,就执行一次固定的操作。

同样是一次鼠标点击,在不同的阶段,RS_ActionDrawLine的处理方式完全不同。例如:

第一次点击 → 确定起点
第二次点击 → 确定终点

为什么同样是鼠标左键点击,却会产生不同的结果?答案就在于:

RS_ActionDrawLine会记录自己当前所处的状态。

这实际上就是一个典型的有限状态机(Finite State Machine,FSM)

3.1、什么是状态机?

可以先不从复杂的理论定义开始理解。所谓状态机,可以简单理解为:

同一个输入,在不同状态下,会产生不同的行为,并可能进入新的状态。

例如,对于画线操作:

当前状态:等待起点
鼠标点击 -> 保存起点 -> 切换状态

此时再次点击:

当前状态:等待终点:
鼠标点击 -> 保存终点 -> 创建直线

虽然两次输入都是:

鼠标左键点击

但是由于当前状态不同,最终执行的逻辑也不同。因此,Action 不能只知道:

“用户点击了鼠标。”

还必须知道:

“用户现在进行到画线流程的哪一步了?”


3.2、RS_ActionDrawLine` 的核心状态

从画线流程来看,可以将RS_ActionDrawLine的状态抽象成两个主要阶段:

需要注意的是,实际源码中的状态名称需要以RS_ActionDrawLine的真实定义为准。但从整个交互逻辑来看,它的核心过程就是:

等待起点 -> 获得起点 -> 等待终点 -> 获得终点 -> 创建直线 -> 继续等待下一次输入


3.3、第一次点击:状态发生第一次转换

刚进入画线 Action 时,可以抽象为:

State = 等待第一点

此时用户点击鼠标:

MousePress

Action 会根据当前状态判断:

“我现在正在等待第一点。”

因此,这次点击的坐标会被解释为:

startPoint

整个过程可以表示成:

因此第一次点击之后:

startPoint = 已确定 endPoint = 未确定

Action 的状态也随之发生变化。


3.4、鼠标移动:状态不变,但数据不断变化

第一次点击之后,Action 已经进入:

等待第二点

此时用户移动鼠标。注意:鼠标移动并不会让状态发生切换。Action 仍然处于:

等待第二点

但是当前鼠标位置会不断变化。因此可以理解为:

状态: MouseMove **等待第二点 ———————> 更新当前坐标 -> 更新预览 -> 仍然等待第二点**

也就是说:

State = 等待第二点
MouseMove -> 更新 currentPoint -> 绘制预览 -> State 不变

这也是状态机中非常常见的一种情况:

输入发生了,但状态本身并没有改变,只更新当前状态相关的数据。


3.5、第二次点击:状态机完成一次循环

当用户第二次点击鼠标时,当前状态已经不是:

等待第一点

而是:

等待第二点

因此,Action 会将当前坐标解释为:

endPoint

然后:

startPoint + endPoint -> 创建 RS_Line

整个过程可以表示成:

为什么最后又回到了“等待第一点”?因为画线工具通常不会在画完一条线之后自动退出。例如用户可以连续绘制:

第一条线 ●────────● 第二条线 ●──────● 第三条线 ●────●

因此,完成一条线之后,Action 会重新进入:

等待第一点

准备下一次绘制。


3.6、用状态转换图理解整个过程

现在把整个RS_ActionDrawLine抽象成一个完整的状态机:

从这个图可以清楚地看到:状态只有两个核心阶段

1、等待第一点
2、等待第二点

而用户的输入会驱动状态发生变化。


3.7、同一个事件,在不同状态下意义不同

这是理解RS_ActionDrawLine最关键的一点。假设收到同样的事件:

MouseButtonPress

如果当前状态是:

等待第一点

那么:

MouseButtonPress -> 设置 startPoint -> 切换到等待第二点

如果当前状态是:

等待第二点

那么:

MouseButtonPress -> 设置 endPoint -> 创建 RS_Line -> 回到等待第一点

因此可以总结为:

当前状态输入事件执行操作下一个状态
等待第一点鼠标点击保存起点等待第二点
等待第二点鼠标移动更新预览终点等待第二点
等待第二点鼠标点击保存终点、创建直线等待第一点

这张表实际上已经完整描述了整个画线 Action 的核心逻辑。


3.8、RS_ActionDrawLine` 为什么要使用状态机?

如果不使用状态,画线逻辑会很难区分:

用户点击

到底意味着:

设置起点? 还是设置终点?

而通过状态变量:

status

或者类似的状态定义,Action 就可以明确知道:

当前正在等待什么输入。

因此整个逻辑可以抽象成:

switch(status){caseSetStartPoint:// 第一次点击startPoint=point;status=SetEndPoint;break;caseSetEndPoint:// 第二次点击endPoint=point;// 创建直线status=SetStartPoint;break;}

上面的代码只是对状态机逻辑的抽象,用来帮助理解,并不代表 LibreCAD 源码中的实际实现。真正的源码可能还会涉及:

  • 捕捉(Snap)
  • 正交模式
  • 坐标限制
  • 取消操作
  • 连续绘制
  • Undo/Redo
  • Preview

因此在阅读源码时,不能只盯着:

ifswitch

而应该关注:

当前 Action 保存了什么状态?什么事件会改变状态?状态改变之后又会触发什么操作?


3.9、从RS_ActionDrawLine看 LibreCAD 的 Action 设计

到这里,其实可以发现一个更重要的结论。RS_ActionDrawLine并不只是:

“画一条线的函数集合。”

它实际上保存了整个用户交互过程。可以理解成:

因此,LibreCAD 中的 Action 本质上更接近:

一个负责管理用户交互流程的状态机。

这也是为什么:

同一个 EventHandler

可以服务于:

画线 画圆 移动 复制 旋转 裁剪

因为 EventHandler 只负责:

把事件交出去。

而不同的 Action 则通过自己的状态机解释这些事件。例如:

鼠标点击
RS_ActionDrawLine -> 设置起点 / 终点
RS_ActionDrawCircle -> 设置圆心 / 半径
RS_ActionMove -> 选择对象 / 确定目标位置

事件本身没有意义,真正赋予事件含义的是当前 Action 以及它所处的状态。


总结

通过RS_ActionDrawLine,我们可以看到一个完整的状态驱动过程:

启动 DrawLine Action -> 等待第一点 -> 第一次点击 -> 记录起点 -> 等待第二点 -> 鼠标移动 -> 更新临时终点 + 显示预览 -> 第二次点击 -> 确定终点 -> 创建 RS_Line -> 重新等待第一点

因此,RS_ActionDrawLine的核心并不是简单地:接收两个坐标,然后画一条线。而是通过状态保存整个交互过程,让同样的鼠标事件在不同阶段产生不同的行为。这也让我们真正理解了 LibreCAD 中 Action 的作用:
Action 不只是“执行一个功能”,更是“管理一次完整用户交互过程的状态机”。

到这里,从用户点击“画线”工具,到第一次点击、鼠标移动、第二次点击,整个交互流程已经基本完整。下一步,就可以继续追踪:
两个坐标确定之后,RS_ActionDrawLine是如何创建RS_Line,并将它加入 LibreCAD 图形数据中的?

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询