问题
由于需要尽可能实现工业软件的画图需求,项目中的连线逻辑为点击两节点或连接桩生成(addEdge)连线,但为了更方便提示用户哪些节点可以生成连线,在连线的操作中还生成了一些虚拟边和为一些节点添加了工具(用于hover改变样式),如
graph.addEdge({
id: 'dummy-edge',
source,
target: { cell: node.id },
shape: selectedDrawRef.current.key,
});
node.addTools([
{
name: 'boundary',
args: {
attrs: {
fill: '#7c68fc',
stroke: '#333',
width: node.getSize().width,
height: node.getSize().height,
'stroke-width': 1,
'fill-opacity': 0.2,
},
},
},
]);
这导致antv x6的undoStack
记录了许多不必要的信息,连线操作无法通过一次undo取消
设计
在意识到这个问题之后,第一想到的是antv x6有没有支持修改操作不计入undoStack的配置参数,但查阅了文档之后发现并没有提供相关api
但是提供了可以把多个改变合并成一个历史记录的api,如下:
// 方式一
graph.startBatch('custom-batch-name')
// 节点改变边框颜色以及修改位置会合并成一条记录,可以一次性撤销
node.attr('body/stroke', 'red')
node.position(30, 30)
graph.stopBatch('custom-batch-name')
// 方式二
graph.batchUpdate(() => {
node.prop('zIndex', 10)
node.attr('label/text', 'hello')
node.attr('label/fill', '#ff0000')
})
虽然对于我们的问题不够直观,但至少可以用来解决撤销连线的问题
回顾连线逻辑的实现
首先回顾我们所写的连线逻辑
记录节点信息数据结构
//连线时节点数据
export interface StartEdgeState {
node: Node;
port?: string;
}
//redux
import { StartEdgeState } from '@/interface/sysml/sysml.interface';
const initialState: StartEdgeState[] = [];
当选中第一个节点(source)时
if (waitEdgeNodes.length === 1) {
//验证该节点是否可以作为起始节点(source),如果不能则清楚状态,直接返回
if (!validateAddEdge(selectedDrawRef.current.key, waitEdgeNodes[0])) {
dispatch(clearSelectDraw());
dispatch(clearSelectNode());
return;
}
//从waitEdgeNodes中获取节点信息
const source = { cell: waitEdgeNodes[0].node.id, port: '' };
//如果起始节点为port,添加port信息
if (waitEdgeNodes[0].port !== undefined) source.port = waitEdgeNodes[0].port;
//绑定节点鼠标移入的监听事件
graph.on('node:mouseenter', ({ node }) => {
//验证可以生成连线后,添加虚拟边dummy-edge
if (
node.id !== waitEdgeNodes[0].node.id &&
validateAddEdge(selectedDrawRef.current.key, waitEdgeNodes[0], { node })
) {
graph.addEdge({
id: 'dummy-edge',
source,
target: { cell: node.id },
shape: selectedDrawRef.current.key,
});
}
});
//绑定节点鼠标移出的监听事件
graph.on('node:mouseleave', ({ node }) => {
//移除虚拟边
graph.removeEdge('dummy-edge');
});
graph.on('node:port:mouseenter', ({ node, port }) => {
//同理
});
graph.on('node:port:mouseleave', () => {
//同理
});
}
当选中第二个节点(target)时
else if (waitEdgeNodes.length === 2) {
//获取起始终止节点信息
const source = { cell: waitEdgeNodes[0].node.id, port: '' };
if (waitEdgeNodes[0].port !== undefined) source.port = waitEdgeNodes[0].port;
const target = { cell: waitEdgeNodes[1].node.id, port: '' };
if (waitEdgeNodes[1].port !== undefined) target.port = waitEdgeNodes[1].port;
//验证是否添加连线,如果验证可以则添加新边
if (validateAddEdge(selectedDrawRef.current.key, waitEdgeNodes[0], waitEdgeNodes[1])) {
graph.addEdge({
source,
target,
shape: selectedDrawRef.current.key,
}),
}
//移除虚拟边,并关闭画布对应监听事件,清楚数据
graph.removeEdge('dummy-edge');
graph.off('node:mouseenter');
graph.off('node:mouseleave');
graph.off('node:port:mouseenter');
graph.off('node:port:mouseleave');
dispatch(clearSelectDraw());
dispatch(clearSelectNode());
}
总体
依赖waitEdgeNodes数组变化,有变化则执行useEffect里的连线逻辑
useEffect(() => {
if (waitEdgeNodes.length === 1) {
//...
}else if (waitEdgeNodes.length === 2) {
//...
}
},[waitEdgeNodes]);
解决
初步
由于能合并历史记录,我们不妨在连线开始的时候,设置合并历史记录的起点,在连线结束时,设置合并历史记录的终点,那么整个中间的连线操作就变成了一个原子操作,可以一次撤销
选择第一个节点时,设置startBatch
graph.on('node:mouseenter', ({ node }) => {
if ( node.id !== waitEdgeNodes[0].node.id && validateAddEdge(selectedDrawRef.current.key, waitEdgeNodes[0], { node })) {
graph.startBatch('addEdge');//合并历史记录起点
}
}
选择第二个节点结束后,设置stopBatch
graph.removeEdge('dummy-edge');
graph.off('node:mouseenter');
graph.off('node:mouseleave');
graph.off('node:port:mouseenter');
graph.off('node:port:mouseleave');
waitEdgeNodes[1].node.removeTools();
graph.stopBatch('addEdge');//合并历史记录终点
这里注意需要判断是否成功添加连线,如果没有成功添加连线,那么这次batch里的历史操作都是连线过程中的冗余操作,会造成一次空undo的情况
if (validateAddEdge(selectedDrawRef.current.key, waitEdgeNodes[0], waitEdgeNodes[1])) {
//...
addedEdge = true;
}
//...
graph.stopBatch('addEdge');
//判断是否成功添加边,如果没有成功添加,手动undo一次冗余操作
if (!addedEdge) {
graph.undo();
}
bug
根据我们的设想应该是解决了,但测试仍然发现当我们在一次连线中多次创建虚拟边后,又变成了无法一次undo的情况
原因是antv6提供的batch
操作有name
做标记,同名的batch
应该会覆盖之间的startbatch
,即当我多次mouseenter
一个节点后,我的batch
操作栈大概如下
batch操作栈 |
---|
graph.stopBatch(‘addEdge’) |
移除dummy-edge |
添加edge |
添加dummy-edge |
graph.startBatch(‘addEdge’) |
添加dummy-edge |
graph.startBatch(‘addEdge’) |
添加dummy-edge |
graph.startBatch(‘addEdge’) |
也就是只有最后的冗余操作合并到了一起,而开头的startBatch
并没有收到stopBatch
的“回应”
所以我们设置一个状态变量来判断是否正在添加连线,只在第一次的入口进行startBatch
//正在添加连线
const isAddingEdge = useRef(false);
//添加起始节点时
if (!isAddingEdge.current) {
graph.startBatch('addEdge');
isAddingEdge.current = true;
}
//完成连线后
isAddingEdge.current = false;
效果
完整代码
const isAddingEdge = useRef(false);
useEffect(() => {
if (waitEdgeNodes.length === 1) {
if (!validateAddEdge(selectedDrawRef.current.key, waitEdgeNodes[0])) {
dispatch(clearSelectDraw());
dispatch(clearSelectNode());
return;
}
const source = { cell: waitEdgeNodes[0].node.id, port: '' };
if (waitEdgeNodes[0].port !== undefined) source.port = waitEdgeNodes[0].port;
graph.on('node:mouseenter', ({ node }) => {
if (
node.id !== waitEdgeNodes[0].node.id &&
validateAddEdge(selectedDrawRef.current.key, waitEdgeNodes[0], { node })
) {
if (!isAddingEdge.current) {
graph.startBatch('addEdge');
isAddingEdge.current = true;
}
graph.addEdge({
id: 'dummy-edge',
source,
target: { cell: node.id },
shape: selectedDrawRef.current.key,
});
node.addTools([
{
name: 'boundary',
args: {
attrs: {
fill: '#7c68fc',
stroke: '#333',
width: node.getSize().width,
height: node.getSize().height,
'stroke-width': 1,
'fill-opacity': 0.2,
},
},
},
]);
}
});
graph.on('node:mouseleave', ({ node }) => {
graph.removeEdge('dummy-edge');
node.removeTools();
});
graph.on('node:port:mouseenter', ({ node, port }) => {
if (
port !== waitEdgeNodes[0].port &&
validateAddEdge(selectedDrawRef.current.key, waitEdgeNodes[0], { node, port })
) {
if (!isAddingEdge.current) {
graph.startBatch('addEdge');
isAddingEdge.current = true;
}
graph.removeEdge('dummy-edge');
graph.addEdge({
id: 'dummy-edge',
source,
target: { cell: node.id, port: port },
shape: selectedDrawRef.current.key,
});
node.removeTools();
}
});
graph.on('node:port:mouseleave', () => {
graph.removeEdge('dummy-edge');
});
} else if (waitEdgeNodes.length === 2) {
const source = { cell: waitEdgeNodes[0].node.id, port: '' };
if (waitEdgeNodes[0].port !== undefined) source.port = waitEdgeNodes[0].port;
const target = { cell: waitEdgeNodes[1].node.id, port: '' };
if (waitEdgeNodes[1].port !== undefined) target.port = waitEdgeNodes[1].port;
let addedEdge = false;
if (validateAddEdge(selectedDrawRef.current.key, waitEdgeNodes[0], waitEdgeNodes[1])) {
graph.unselect(waitEdgeNodes[1].node);
graph.select(
graph.addEdge({
source,
target,
shape: selectedDrawRef.current.key,
}),
);
addedEdge = true;
}
graph.removeEdge('dummy-edge');
graph.off('node:mouseenter');
graph.off('node:mouseleave');
graph.off('node:port:mouseenter');
graph.off('node:port:mouseleave');
waitEdgeNodes[1].node.removeTools();
graph.stopBatch('addEdge');
if (!addedEdge) {
graph.undo();
}
isAddingEdge.current = false;
dispatch(clearSelectDraw());
dispatch(clearSelectNode());
}
}, [waitEdgeNodes]);
0 条评论