撤销是将上一个动作造成的影响消除,而重做是将消除的动作的影响重新添加。
撤销和重做通过两个队列进行管理。分为actionQueue和backQueue
当进行撤销动作的时候,将actionQueue的末尾action弹出到backQueue的末尾中,并执行撤销动作
当进行重做动作的时候,将backQueue的末尾action弹出到actionQueue的末尾中,并执行重做动作
需要注意的是,有的动作不会产生影响,所以不应加入队列,有的动作为连续多次发生,如果全部加入到队列当中,可能对功能造成极大的影响,所以需要进行合并操作。
所以首先进行队列类的设计和action类的设计。
1.队列类的设计
actionQueue和backQueue作为队列管理
filter筛去不应进入队列的类
pushAction将action推入actionQueue中
逻辑为,首先将action的后续操作做完,然后发出P信号允许下一个信号发出,同时使用filter筛去动作,判断上一个动作和这一个是否可以合并,清空backQueue,并发出discard信号
forwardAction和backAction首先判断队列中是否有action,然后进行撤销动作
updateMapAction用来更新ui按钮状态。
class ActionQueue {
constructor() {
this.actionQueue=[];
this.backQueue=[];
}
filter(action){
//筛去不进入队列的action
}
pushAction(action){
action.after();
V();
if(this.filter(action)) {
if(this.actionQueue.length>0){
if(action.merge(this.actionQueue[this.actionQueue.length-1])){
this.actionQueue.pop();
}
}
this.actionQueue.push(action);
if(this.backQueue.length>0){
let time=getActionCounter();
P("discard",{time:time});
}
this.backQueue=[];
}
this.updateMapAction()
}
backAction(){
if(this.actionQueue.length>0){
console.log(this.actionQueue);
console.log(this.backQueue);
this.actionQueue[this.actionQueue.length-1].backward();
this.backQueue.push(this.actionQueue[this.actionQueue.length-1]);
this.actionQueue.pop();
// console.log(this.actionQueue);
// console.log(this.backQueue);
}
this.updateMapAction();
}
forwardAction(){
if(this.backQueue.length>0){
console.log(this.actionQueue);
console.log(this.backQueue);
this.backQueue[this.backQueue.length-1].forward();
this.actionQueue.push(this.backQueue[this.backQueue.length-1]);
this.backQueue.pop();
// console.log(this.actionQueue);
// console.log(this.backQueue);
}
this.updateMapAction();
}
updateMapAction(){
let msg={}
msg['back']=this.actionQueue.length !== 0;
msg['up']=this.backQueue.length !== 0;
color_change_bar(msg);
}
}
2.action类的设计
action类统一继承base_action类,方法有
create:向后端发送信号
after:在后端执行完成之后需要进行的操作
forward:重做操作
backward:撤销操作
merge:合并操作,是否选择和actionQueue的前一个动作进行合并,优化用户体验。
fliter:过滤器,过滤不需要记录在actionQueue队列中的操作
3.对于各个类的设计逻辑
A.不进入actionQueue队列中的类
这些类不需要进入队列,只需设计after操作即可
B.需要进入actionQueue队列中的类但无需记录状态
如move和movep,只需要根据信号进行反推即可
C.需要进入actionQueue队列中的类且需记录状态
如set_html,set_theta,直接设置值的操作,需要进行类的设计
4.对部分类的逻辑解读
A.back\backward\front\forward
置底置顶操作入队列,但是撤销和重做的实现较为简单。
Eg:baskAction
after(){
this.core=getCoreList();
}
forward(){
P("cursors",{ids:this.core},false);
P("back",{},false);
}
backward(){
P("cursors",{ids:this.core},false);
P("front",{},false);
}
B.getP
取得关键点的after函数有许多重要的部分
after(){
setCore(this.points)
changeModel();
P("get_center",{})
}
首先通过points进行关键点的绘制和coreQueue的确认和维护
changeModel为右侧工具栏的切换
get_center为得到组件的旋转中心进行设置。
C.get_center
after(){
let coreList=getCoreList();
console.log(coreList);
for(let i=0;i<coreList.length;i++){
let element=getModuleByGid(coreList[i]);
element.center_x=this.msg['centers'][i]['x']
element.center_y=this.msg['centers'][i]['y']
setTreeSonCenter(coreList[i],{x:this.msg['centers'][i]['x'],y:this.msg['centers'][i]['y']})
}
}
center从组件树上往下对组件进行center赋值
D.move和moveP
after(){
for(let i=0;i<this.ids.length;i++){
updateGuide(this.ids[i])
guideSet(this.ids[i])
update_position_by_gid(this.ids[i])
let list=getChildren(this.ids[i])
for(let j=0;j<list.length;j++){
update_position_by_gid(list[j])
}
}
}
位置状态改变后,需要更新引导线的位置,锚点的位置,同时children的锚点也要进行修改
merge(action){
// console.log(action);
if(action.type==="move"){
let map={}
for(let i=0;i<this.ids.length;i++){
map[this.ids[i]]=1;
}
for(let i=0;i<action.ids.length;i++){
if(map[action.ids[i]]===undefined)return false;
}
this.dx+=action.dx;
this.dy+=action.dy;
return true;
}
else return false;
}
merge操作需要查看操作list是否完全一致,如果完全一致,则进行合并
E.get_rect/get_html/get_alignment/get_spacing/get_theta
需要用在右侧的状态栏更新,所以需要进行一定的设置
Eg:get_rect
after(){
let msg={}
msg['flag']=true;
msg['x']=this.msg['rect']['x']
msg['y']=this.msg['rect']['y']
msg['height']=this.msg['rect']['height']
msg['width']=this.msg['rect']['width']
setPosition(msg);
setElementSize(msg);
}
F.add
这应该是最为复杂的动作设计,需要区分line,group生成的伪矩阵。
after(){
this.msg['show']=this.cmd['show'];
if(!this.cmd['show']){
let node=document.getElementById(this.id);
let parser=new cssParser();
parser.parseStyle(node.getAttribute("style"))
parser.updateStyle({display:"none"})
parser.updateStyle({'stroke-width':0})
node.setAttribute("style",parser.get());
createModule(this.msg,false);
addModuleToTree(this.id,this.type);
let coreList=getCoreList();
for (var i=0;i<coreList.length;i++){
linkByGroup(coreList[i],this.id);
}
P("cursors",{ids:[this.msg['id']]})
P("cover_children",{})
P("enable_scale_bind",{})
return;
}
if(this.type==="line"){
createLine(this.msg);
}else{
createModule(this.msg);//需要修改
}
P("cursors",{ids:[this.msg['id']]})
let move=set_move_center();
let bbox=document.getElementById(this.id).getBBox();
let msg={g_id:getCoreList()[0],move_x:move['x']-bbox.width/2,move_y:move['y']-bbox.height/2}
P("move",msg,false)
addModuleToTree(this.id,this.type);
// anchor_add(this.id);
}
G.cursors
cursors选中后只需get_p即可