撤销和重做
撤销和重做操作主要的两种实现方式:
- 记录每一步操作后的当前数据库,撤销时把保存的数据库恢复。这种方式比较消耗内存;
- 记录每一步操作前后的差异,撤销时对差异进行“减法”操作,重做时对差异进行“加法”操作。但是有时难以计算“差异”。
撤销类
RS_Undoable
RS_Undoable
继承了标志类RS_Flags
,添加了一些设置标志类中的撤销标志位的方法。并且预留了一个虚函数接口void undoStateChanged(bool undone);
,用于设置撤销标志位变换之后的操作。
继承自RS_Undoable
类是实体类RS_Entity
,这表明每一个实体类都是支持Undo/Redo
操作的。在RS_Entity
类中,undoStateChanged()
函数主要就是将实体的选中标志位设置为false
,然后调用update()
虚函数。RS_Entity::update()
只提供了一个空实现,而派生类中重写了update()
函数的类不多,例如基本的绘图实体(直线、圆、圆弧)都没有重写,所以一般实体在执行Undo
操作时,就是将自身的选中标志位设置为false
就完了。
void RS_Entity::undoStateChanged(bool undone)
{
Q_UNUSED( undone);
setSelected(false); // 为什么不使用传入参数undone??
update();
}
RS_UndoCycle
撤销循环类RS_UndoCycle
相当于执行一个特征对应的节点。RS_UndoCycle
的定义如下,类中有一个RS_Undoable*
的集合,这个集合里面存放的实际上是实体的指针。在实际操作过程中,每执行一个特征后,都会添加一个撤销循环对象,这里面记录了该特征实际产生了多少个可撤销的步骤。因为有些特征包含多个步骤,所以这里使用了集合来存放RS_Undoable
。例如添加一条直线段,撤销循环中就只会有一个RS_Undoable
;如果选中多个实体,然后删除掉它们,撤销循环中就会有多个RS_Undoable
,每个RS_Undoable
对应一个实体删除的记录。
class RS_UndoCycle {
public:
RS_UndoCycle(/*RS2::UndoType type*/)=default;
void addUndoable(RS_Undoable* u);
void removeUndoable(RS_Undoable* u);
size_t size(void);
void changeUndoState(); // 各自执行undo操作
// ...
private:
std::set<RS_Undoable*> undoables; // 实际存放的是实体指针
};
RS_Undo
RS_Undo
类是与撤销操作相关的管理类,所有的撤销操作都是从这里开始执行的。RS_Undo
类提供了undo()
和redo()
两个接口,分别用来执行撤销和重做的操作。RS_Undo
类内存储了一个撤销列表,撤销列表是vector
类型,表明撤销列表有顺序的要求;里面的元素是RS_UndoCycle
的共享指针,表明每一个撤销节点。提供了一个计数器undoPointer
用于表示当前撤销节点的位置,默认情况下执行最后一个撤销节点。
class RS_Undo {
public:
virtual ~RS_Undo() = default;
bool undo();
bool redo();
int countUndoCycles(); // 可以undo的步骤的数量
int countRedoCycles(); // 可以redo的步骤的数量
bool hasUndoable(); // 是否可以执行undo
void startUndoCycle(); // 开始添加新的Undo,将已经撤销过的节点删掉
void addUndoable(RS_Undoable* u);
virtual void endUndoCycle();
virtual void removeUndoable(RS_Undoable* u) = 0;
// ...
private:
std::vector<std::shared_ptr<RS_UndoCycle>> undoList; // 撤销列表
int undoPointer = -1; // 当前节点指针
// ...
};
从RS_Undo
类派生的类是RS_Document
文档类。文档类在执行撤销特征的时候,会调用undo()
函数。在undo()
函数中,在获取当前撤销节点的同时,更新当前撤销节点指针,然后更新界面上的撤销和重做图标,最后调用当前撤销节点的执行撤销操作的函数。redo()
函数的操作与该函数类似。
bool RS_Undo::undo() {
// ...
std::shared_ptr<RS_UndoCycle> uc = undoList[undoPointer--]; // 获取当前撤销节点,更新当前撤销节点指针
setGUIButtons(); // 更新撤销和重做的图标
uc->changeUndoState(); // 当前撤销节点(撤销循环)执行撤销操作
return true;
}
RS_Undo
类中提供的startUndoCycle()
、addUndoable()
、endUndoCycle()
三个函数通常是在一起使用的。startUndoCycle()
函数中会做一些清理的工作,会删掉已经撤销掉的操作记录。addUndoable()
函数将待撤销的实体添加到当前撤销循环中,endUndoCycle()
函数将当前撤销循环添加到撤销列表中。
撤销类的对应关系
RS_Undoable
与实体对应;RS_UndoCycle
与特征对应;RS_Undo
与文档对应;
绘制实体的撤销
以绘制直线段为例,说明绘制实体的撤销过程。
添加撤销节点
添加撤销节点是在特征执行完成后进行的。在特征类的trigger()函数中,会根据特征输入的参数创建一个实体。然后更新撤销列表。最后更新当前视图。因为默认情况下,实体的撤销标志位是置空的,所以该实体此时在视图中是正常显示的。
void RS_ActionDrawLine::trigger() // 直线特征执行完成后,调用该函数
{
RS_PreviewActionInterface::trigger();
RS_Line* line = new RS_Line(container, pPoints->data); // 创建直线实体
line->setLayerToActive();
line->setPenToActive();
container->addEntity(line);
// update undo list 更新撤销列表。
if (document) { // document为当前特征作用的文档,撤销列表是document类的一个基类,可以直接调用撤销类的函数
document->startUndoCycle(); // 准备添加撤销节点。会清理掉已经撤销过的节点(如果有)。
document->addUndoable(line); // 将当前创建的直线实体添加到文档中
document->endUndoCycle(); // 结束
}
graphicView->redraw(RS2::RedrawDrawing); // 更新视图
// ...
}
执行撤销操作
接收到撤销的特征执行命令后(通过界面撤销按钮或按Ctrl+Z
按键),就只直接执行撤销特征的trigger()
函数。在RS_ActionEditUndo::tirgger()
函数中通过调用文档对象调用undo()
函数。
void RS_ActionEditUndo::trigger()
{
// ...
if (undo) {
if(!document->undo()) // 通过文档调用undo()函数,执行撤销步骤
RS_DIALOGFACTORY->commandMessage(tr("Nothing to undo!"));
} else {
if(!document->redo()) // 重做
RS_DIALOGFACTORY->commandMessage(tr("Nothing to redo!"));
}
graphic->addBlockNotification();
graphic->setModified(true);
document->updateInserts();
graphicView->redraw(RS2::RedrawDrawing); // 重绘视图
finish(false);
RS_DIALOGFACTORY->updateSelectionWidget(container->countSelected(),
container->totalSelectedLength());
}
而在RS_Undo::undo()
函数中,最重要的就是通过调用撤销循环类的changeUndoState()
函数来更改每一个实体的撤销标识位。切换标识位使用的toggleFlag()
方法,toggle
的意思是:如果该标识位为1,就将其修改为0;如果是0,就修改为1。
void RS_UndoCycle::changeUndoState()
{
for (RS_Undoable* u: undoables) // 为撤销循环中的每一个实体执行修改撤销标识位的操作
u->changeUndoState();
}
void RS_Undoable::changeUndoState() {
toggleFlag(RS2::FlagUndone); // 切换撤销标志位。toggle:如果是0,修改为1;如果是1,修改为0;
undoStateChanged(isUndone()); // 实际上是调用重绘函数
}
重绘撤销过的实体
在撤销特征执行函数RS_ActionEditUndo::trigger()
后面,调用了视图的redraw
函数来执行视图重绘。最后会调用如下函数:
void RS_GraphicView::drawEntity(RS_Painter *painter, RS_Entity* e, double& patternOffset)
在该函数中会先通过RS_Entity::isVisible()
函数来判断实体是否可见,然后检查撤销标识位,如果撤销标识位为true
,就会直接返回,不重绘当前实体。
bool RS_Entity::isVisible() const{
if (!getFlag(RS2::FlagVisible)) { // 先检查visible标识位
return false;
}
if (isUndone()) { // 再检查撤销标识位,如果为true,直接返回当前不可见(即不重绘当前实体)
return false;
}
// ...
}
所以被撤销的实体并不是被删除了,只是没有显示出来而已。
修改实体的撤销
下面以剪切特征的撤销过程来说明实体修改功能如何实现撤销,同时也分析剪切功能的实现方式。
剪切特征的状态
剪切功能对应的特征类是RS_ActionModifyTrim
类,该特征类在执行时有两个状态,第一个表示限制实体,第二个表示被修剪的实体。
enum Status {
ChooseLimitEntity, /**< Choosing the limiting entity. */
ChooseTrimEntity /**< Choosing the entity to trim. */
};
选中待剪切实体
在修剪特征类的鼠标释放按钮的响应函数中,实现了对实体选择的响应,记录了选中的限制实体和修剪实体。
void RS_ActionModifyTrim::mouseReleaseEvent(QMouseEvent* e) {
if (e->button()==Qt::LeftButton) {
RS_Vector mouse = graphicView->toGraph(e->x(), e->y());
RS_Entity* se = catchEntity(e); // 首先获取鼠标拾取到的实体。有可能没有拾取到实体,则返回nullptr
switch (getStatus()) {
case ChooseLimitEntity: // 第一步
pPoints->limitCoord = mouse;
limitEntity = se; // 拾取到的第一个实体
if (limitEntity && limitEntity->rtti() != RS2::EntityPolyline/*&& limitEntity->isAtomic()*/) { // 不是多段线
limitEntity->setHighlighted(true);
graphicView->drawEntity(limitEntity); // drawEntity()的参数没有被使用。还是会调用redraw()刷新所有实体的显示。
setStatus(ChooseTrimEntity); // 切换到下一步
}
break;
case ChooseTrimEntity: // 第二步
pPoints->trimCoord = mouse;
trimEntity = se; // 拾取到的第二个实体
if (trimEntity && trimEntity->isAtomic()) {
trigger(); // 执行修剪功能
}
break;
default:
break;
}
}
// ...
}
执行剪切功能
执行剪切功能的函数是RS_ActionModifyTrim::trigger()
,在该函数中,借用RS_Modification
类来实现剪切的功能。RS_Modification
类是一个实体修改类,该类提供了所有的实体修改功能,包括复制、粘贴、旋转、缩放、修剪、拉伸等等。
在修剪功能函数中,执行修剪功能流程如下,具体参阅trim函数代码和注释。
- 函数参数是修剪特征中选中的实体和选择点(选中实体时鼠标点击的位置)。
both
参数表示是不是两个实体都修剪; - 首先判断限制实体
limitEntity
是不是一个可修剪实体,则输出警告(最后也不会修剪限制实体); - 调用
findIntersection
计算两个实体的交点,两个实体的交点可能有多个。RS_VectorSolutions
实际上就是点的集合; - 如果没有有用的交点,并且两个实体都需要修剪的情况下,会变换两个实体的参数位置,重新执行依次修剪,并且将
both
参数设置为false; - 定义两个原子实体指针
trimmed1
和trimmed2
,用来表示被修剪后的两个实体。trimmed1
指向待修剪实体的克隆,trimmed2
指向限制实体的克隆。 - 计算交点容器中距离鼠标点击的位置的最近交点,这个点是用来决定要剪切掉实体的哪一部分的;
- 实体修剪是先通过
getTrimPoint()
计算修剪的点,然后判断这个点距离哪个端点更近(实体的起点或终点)。真正修剪的时候,并不是将实体一分为二,而是通过修改实体的起点或终点,来实现修改的效果。比如,修剪一条直线段,如果鼠标点击的位置距离终点更近,就会将直线段的终点坐标修改为交点位置,即可表现为直线段被修剪过一样。 - 将修剪后实体
trimmed1
和trimmed2
添加到实体容器中,重绘视图; - 更新修改撤销列表。
bool RS_Modification::trim(const RS_Vector& trimCoord,
RS_AtomicEntity* trimEntity, // 第二个实体(待修剪的实体)
const RS_Vector& limitCoord,
RS_Entity* limitEntity, // 第一个实体(both为false时,不修剪)
bool both) {
// ...
// 函数isAtomic()判断是否为原子实体。原子实体表示由单一的实体构成。非单一实体由多个实体组成(例如多段线、矩形),非单一实体不能被修剪
if (both && !limitEntity->isAtomic()) {
RS_DEBUG->print(RS_Debug::D_WARNING,
"RS_Modification::trim: limitEntity is not atomic");
}
if(trimEntity->isLocked()|| !trimEntity->isVisible()) return false;
RS_VectorSolutions sol = findIntersection(*trimEntity, *limitEntity);
//删除交点为直线起点或终点的情况。
//if intersection are in start or end point can't trim/extend in this point, remove from solution. sf.net #3537053
if (trimEntity->rtti()==RS2::EntityLine){
RS_Line *lin = (RS_Line *)trimEntity;
for (unsigned int i=0; i< sol.size(); i++) {
RS_Vector v = sol.at(i);
if (v == lin->getStartpoint())
sol.removeAt(i);
else if (v == lin->getEndpoint())
sol.removeAt(i);
}
}
if (!sol.hasValid()) { // 没有有用的交点,变换两个实体的参数位置,重试trim
return both ? trim( limitCoord, (RS_AtomicEntity*)limitEntity, trimCoord, trimEntity, false) : false;
}
RS_AtomicEntity* trimmed1 = nullptr;
RS_AtomicEntity* trimmed2 = nullptr;
if (trimEntity->rtti()==RS2::EntityCircle) {
// convert a circle into a trimmable arc, need to start from intersections
trimmed1 = trimCircle(static_cast<RS_Circle*>(trimEntity), trimCoord, sol);
} else {
trimmed1 = (RS_AtomicEntity*)trimEntity->clone(); // 克隆(拷贝)实体
trimmed1->setHighlighted(false);
}
// trim trim entity
size_t ind = 0;
RS_Vector is, is2;
//RS2::Ending ending = trimmed1->getTrimPoint(trimCoord, is);
if ( trimEntity->trimmable() ) {
is = trimmed1->prepareTrim(trimCoord, sol); // 最近点
} else {
is = sol.getClosest(limitCoord, nullptr, &ind); // 计算交点容器中的最近点
//sol.getClosest(limitCoord, nullptr, &ind);
RS_DEBUG->print("RS_Modification::trim: limitCoord: %f/%f", limitCoord.x, limitCoord.y);
RS_DEBUG->print("RS_Modification::trim: sol.get(0): %f/%f", sol.get(0).x, sol.get(0).y);
RS_DEBUG->print("RS_Modification::trim: sol.get(1): %f/%f", sol.get(1).x, sol.get(1).y);
RS_DEBUG->print("RS_Modification::trim: ind: %lu", ind);
is2 = sol.get(ind==0 ? 1 : 0); // 最近点另外一个点
//RS_Vector is2 = sol.get(ind);
RS_DEBUG->print("RS_Modification::trim: is2: %f/%f", is2.x, is2.y);
}
// remove trim entity from view:
if (graphicView) {
graphicView->deleteEntity(trimEntity);
}
// remove limit entity from view:
bool trimBoth= both && !limitEntity->isLocked() && limitEntity->isVisible();
if (trimBoth) {
trimmed2 = (RS_AtomicEntity*)limitEntity->clone(); // 克隆(拷贝)实体
trimmed2->setHighlighted(false);
if (graphicView) {
graphicView->deleteEntity(limitEntity); // 实际上并没有删掉实体,只是调用了重绘函数
}
}
RS2::Ending ending = trimmed1->getTrimPoint(trimCoord, is);
switch (ending) {
case RS2::EndingStart:
trimmed1->trimStartpoint(is); // 一般是修改当前实体的数据来实现修剪。比如修剪线段,就是直接修改线段的起点或终点坐标
break;
case RS2::EndingEnd:
trimmed1->trimEndpoint(is);
break;
default:
break;
}
// trim limit entity:
if (trimBoth) {
if ( trimmed2->trimmable())
is2 = trimmed2->prepareTrim(limitCoord, sol);
else
is2 = sol.getClosest(trimCoord);
RS2::Ending ending = trimmed2->getTrimPoint(limitCoord, is2);
switch (ending) {
case RS2::EndingStart:
trimmed2->trimStartpoint(is2);
break;
case RS2::EndingEnd:
trimmed2->trimEndpoint(is2);
break;
default:
break;
}
}
// add new trimmed trim entity:
container->addEntity(trimmed1);
if (graphicView) {
graphicView->drawEntity(trimmed1);
}
// add new trimmed limit entity:
if (trimBoth) {
container->addEntity(trimmed2);
if (graphicView) {
graphicView->drawEntity(trimmed2);
}
}
if (handleUndo) {
LC_UndoSection undo( document);
undo.addUndoable(trimmed1);
trimEntity->setUndoState(true); // 设置undo状态实际上就是setSelected(false),将该实体设置为未被选中
undo.addUndoable(trimEntity);
if (trimBoth) {
undo.addUndoable(trimmed2);
limitEntity->setUndoState(true);
undo.addUndoable(limitEntity);
}
}
return true;
}
更新撤销列表
更新撤销列表部分的代码如下,具体流程为:
- 创建一个
LC_UndoSection
类对象,用于控制撤销列表添加撤销节点(撤销循环RS_UndoCycle
)的过程。在LC_UndoSection
类的构造函数中调用了RS_Undo::startUndoCycle()
函数,在析构函数中调用了RS_Undo::endUndoCycle()
函数; - 将修剪后的两个实体
trimmed1
和trimmed2
添加到撤销节点中; - 设置修剪前的两个实体
trimEntity
和limitEntity
的撤销状态为true
,表示该实体此时未被选中,即不显示; - 将修剪前的两个实体也添加到撤销节点中。
所以一个修剪特征的撤销节点中包含四个撤销对象,分别为修剪的两个实体和修剪后的两个实体。在显示的时候,显示修剪后的两个实体。如果执行了undo
操作,就显示修剪前的两个实体,而且隐藏修剪后的两个实体。
if (handleUndo) {
LC_UndoSection undo( document); // 使用RAII技巧控制撤销节点添加的过程
undo.addUndoable(trimmed1); // 修剪后的实体,添加到撤销列表中
trimEntity->setUndoState(true); // 设置undo状态实际上就是setSelected(false),将该实体设置为未被选中
undo.addUndoable(trimEntity); // 修剪前的实体,添加到撤销列表中
if (trimBoth) {
undo.addUndoable(trimmed2);
limitEntity->setUndoState(true);
undo.addUndoable(limitEntity);
}
}
总结
在LibreCAD
中,撤销和重做的功能的实现方式是通过设置实体的显示或隐藏来实现的。
这种方式在二维CAD
中尚且可以使用,在复杂一点的三维CAD
中基本上不可用的。例如最常见的三维实体的布尔运算,实体A与实体B相加得到实体C,但是实体C减去实体A通常并不等于实体B。
另外LibreCAD
不支持非原子实体的修剪,比如不支持直线段与矩形的修剪功能。所以LibreCAD
的功能也只是很基础的。