👓️博主简介:
文章目录
- 前言
- 一、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 → currentPoint2.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、到这里,画线过程已经越来越清晰
现在把前三个阶段放在一起:
- 点击“画线”:
QAction -> ActionHandler -> RS_ActionDrawLine- 第一次点击:
QMouseEvent -> EventHandler -> RS_ActionDrawLine -> 确定起点- 鼠标移动:
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 = 等待第一点
此时用户点击鼠标:
MousePressAction 会根据当前状态判断:
“我现在正在等待第一点。”
因此,这次点击的坐标会被解释为:
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 图形数据中的?
🎇坚持到这里已经很厉害啦,辛苦啦🎇 ʕ • ᴥ • ʔ づ♡ど