我们的需求:给连接桩赋不同样式与数据代表不同类型,连线中只有相同类型的连接桩才可以相连,并且左边连接桩为输入,右边连接桩为输出,输入必须和输出相连,输出也只能和输入相连,每次选择节点后回传数据
假设我们的数据结构如下;
{
"name": "python",
"icon": "images/pythonScripts@2x.png",
"icon1": "images1/pythonScripts@2x.png",
"description": "",
"category": "Scripts",
"typeID": 3,
"inputs": [
{
"name": "Data",
"type": "DataType"
},
{
"name": "Model",
"type": "ModelType"
}
],
"outputs": [
{
"name": "Data",
"type": "DataType"
},
{
"name": "Model",
"type": "ModelType"
}
]
},
然后我们先来看:
let groups = {},
items = [],
inputs = type.inputs,
outputs = type.outputs,
name = type.name;
if (inputs.length) {
inputs.forEach((item, index) => {
// 如果type为data类型
if (item.type === "dataType") {
groups.input1 = {
position: {
name: "absolute", // 连接桩固定属性
},
attrs: {
fo: {
magnet: "true",
},
data: item, // 自定义与与节点/边关联的业务数据
},
};
items.push({
id: `${name}_input_${item.name}_${item.type}`, // 使用拼接字符串代表此连接桩属性
group: "input1",
args: { // 连接桩位置
x: 0,
y: 47,
angle: 45,
},
});
}
// 如果type为model类型
if (item.type === "" ) {
groups.input2 = {
position: {
name: "absolute",
},
attrs: {
fo: {
magnet: "true",
},
data: item, // 自定义与与节点/边关联的业务数据,为我们上方展示的数据结构
},
};
items.push({
id: `${name}_input_${item.name}_${item.type}`,
group: "input2",
args: {
x: 0,
y: 104,
angle: 45,
},
});
}
});
}
if (outputs.length) {
outputs.forEach((item, index) => {
// 如果type为data类型
if (item.type === "" ) {
// console.log(123123123123123123123123123123123123123123);
groups.output1 = {
position: {
name: "absolute",
},
attrs: {
fo: {
magnet: "true",
},
data: item, // 自定义与与节点/边关联的业务数据
},
};
items.push({
id: `${name}_output_${item.name}_${item.type}`,
group: "output1",
args: {
x: 99,
y: 47,
angle: 45,
},
});
}
// 如果type为model类型
if (item.type === model") {
groups.output2 = {
position: {
name: "absolute",
},
attrs: {
fo: {
magnet: "true",
},
data: item, // 自定义与与节点/边关联的业务数据
},
};
items.push({
id: `${name}_output_${item.name}_${item.type}`,
group: "output2",
args: {
x: 99,
y: 104,
angle: 45,
},
});
}
});
}
const ports = {// 写入port
groups: groups,
items: items,
};
然后来看我们的自定义连接桩,这里使用的react组件
import React from "react";
import { Tooltip } from 'antd';
import { insertCss } from "insert-css";
import nodeWrap from '@/assets/images/nodeWrap@2x.png'
const imgBaseUrl = "http://192.168.19.107:800/";
export const CustomizeNode = ({ type }) => {
return (
<div className="AINodeWrap">
<Tooltip title={type.name}>
<div className="AINodeTitle">{type.name}</div>
</Tooltip>
<div className="AINodeContent">
<img className="AIIcon" src={`${imgBaseUrl}${type.icon1}`} alt=''></img>
</div>
</div>
);
};
insertCss(`
.AINodeWrap {
width: 100px;
height: 130px;
background-image: url(${nodeWrap});
background-size: 100px 130px;
}
.AINodeTitle {
width: 100%;
height: 24px;
border-radius: 8px 8px 0px 0px;
font-size: 12px;
font-family: PingFangSC-Semibold, PingFang SC;
font-weight: 600;
color: #F9FDFF;
line-height: 24px;
text-align: center;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.AINodeContent {
width: 100%;
height: 102px;
border-radius: 0 0 8px 8px;
display: flex;
justify-content: center;
align-items: center;
}
.AIIcon {
width: 98px;
height: 112px;
padding-top: 25px;
}
.text {
font-size: 14px;
margin-top: 12px;
font-family: Helvetica;
font-weight: ;
font-size: 14px;
color: #040D26;
letter-spacing: 0;
text-align: center;
line-height: 12px;
}
`);
然后初始化拖拽画布,节点与连接桩(自定义节点我们之前写过了)
const target = graph.createNode({
width: 100,
height: 130,
shape: "react-shape",
component: <CustomizeNode type={type} />,
event: "node:dblclick",
data: type,
portMarkup: [Markup.getForeignObjectMarkup()], // 链接桩的 DOM 结构,当 ports.groups 和 ports.items 都没有为对应的链接桩指定 markup 时,则使用这个默认选项来渲染链接桩
ports: ports,
});
dnd.start(target, e.nativeEvent);
这里一定不要忘了在生成画布的时候把自定义连接桩引入
const graph = new Graph({
container: container,
width: "100%",
height: "100%",
background: {
color: "#383838", opacity: "0.5"
},
onPortRendered(args) { // 自定义连接桩
const selectors = args.contentSelectors
const container = selectors && selectors.foContent
const portAttr = args.port.attrs.data
if (container) {
const root = ReactDOM.createRoot(container);
root.render(
<Tooltip title={portAttr.type}>
<CustomizePort portAttr={portAttr}></CustomizePort> {/* eslint-disable-next-line */}
</Tooltip>,
);
}
}
})
然后我们来看连线,连线的时候呢,我们不能规定哪条线连不上,只有让它连上以后再取消操作
graph.on(
"edge:connected",
({ e, isNew, edge, previousCell, currentCell }) => {
const source = edge.getSourceCell();
// 删除之后也会调用这个方法,source为空
if (!source) {
return;
}
let json = graph.toJSON();
console.log("连接边的操作", json, json.cells);
// 判断是否连接的目标节点的输入点
if (edge.shape === "dag-edge") {
const edgeSource = edge.source.port
const edgeTarget = edge.target.port
const edgeInput = allTrim(edgeSource.split('_')[3]) // 从字符串中把类型取出来
const edgeOutput = allTrim(edgeTarget.split('_')[3])
if (
// 判断是否是输出节点和输入节点相连
edgeSource.indexOf("output") === -1 ||
edgeTarget.indexOf("input") === -1
|| edgeInput !== edgeOutput // 或者input和output类型不相同
) {
graph.removeEdge(edge.id); // 取消连接边的操作
}
}
setGraphJSON(json);
}
);
记得连线规则要在初始化画布时配置哦~
const graph = new Graph({
container: container,
width: "100%",
height: "100%",
background: {
color: "#383838", opacity: "0.5"
},
connecting: {
// 配置全局的连线规则
snap: true, // 是否自动吸附
allowMulti: true, // 是否允许在相同的起始节点和终止之间创建多条边
allowNode: false, // 是否允许边链接到节点(非节点上的链接桩)
allowBlank: false, // 是否允许连接到空白点
allowLoop: false, // 是否允许创建循环连线,即边的起始节点和终止节点为同一节点,
allowEdge: false, // 是否允许边链接到另一个边
highlight: true, // 拖动边时,是否高亮显示所有可用的连接桩或节点
connector: "algo-connector", // 边渲染到画布后的样式
connectionPoint: "anchor", // 指定连接点
anchor: "center", // 指定被连接的节点的锚点
// validateMagnet({ e, magnet, view, cell }) {
// // magnet 被按下时,是否创建新的边
// console.log("magent", e, magnet, view, cell);
// return false;
// },
createEdge() {
// 连接的过程中创建新的边
return graph.createEdge({
shape: "dag-edge",
attrs: {
line: {
strokeDasharray: "5 5",
},
},
zIndex: -1,
});
},
},
})
还有连线样式
// 连接过程中产生的新边的样式
Graph.registerEdge(
"dag-edge",
{
inherit: "edge",
attrs: {
line: {
stroke: "#9DADB6",
strokeWidth: 2,
sourceMarker: null,
targetMarker: {
//
name: "block", // 实心箭头
},
},
},
},
true
);
graph.on("cell:selected", ({ cell, options }) => {
const allSelected = graph.getSelectedCells();
console.log('allSelected============', allSelected)
setSelectJSON(allSelected);
});
// 画布选择数据重新变为画布全部cell
graph.on("cell:unselected", ({ cell, options }) => {
let json = graph.toJSON();
console.log("取消选中", json);
setSelectJSON();
});
注意,使用框选也要在画布初始化时配置哦
const graph = new Graph({
container: container,
width: "100%",
height: "100%",
background: {
color: "#383838", opacity: "0.5"
},
selecting: { // 配置框选
enabled: true,
className: 'my-selecting', // 附加样式名,用于定制样式,
multiple: true, // 是否启用点击多选,默认为 true。启用多选后按住 ctrl 或 command 键点击节点实现多选。
rubberband: false, // 是否启用框选
rubberNode: true, // 自定义框选节点
rubberEdge: true, // 自定义框选边
movable: true, // 在多选情况下,选中的节点是否一起移动
following: true, // 在多选情况下,选中的节点是否跟随鼠标实时移动
showNodeSelectionBox: true, // 是否显示节点的选择框
showEdgeSelectionBox: true, // 是否显示边的选择框
strict: true, // 启用框选时,选框完全包围节点时才选中节点,否则只需要选框与节点的包围盒(BBox)相交即可选中节点
content: (selection) => {
return StringExt.template(
'<%= length %> node<%= length > 1 ? "s":"" %> selected.',
)({ length: selection.length })
}
},
});
最后,怕大家看的懵,我再贴一个完整的画布初始化配置上来吧~
//官方文档写的是componentDidMount,因为react取消了三个生命周期函数,所以使用useEffect
useEffect(() => {
// 连接过程中产生的新边的样式
Graph.registerEdge(
"dag-edge",
{
inherit: "edge",
attrs: {
line: {
stroke: "#9DADB6",
strokeWidth: 2,
sourceMarker: null,
targetMarker: {
//
name: "block", // 实心箭头
},
},
},
},
true
);
// 自定义连接器,将起点、路由返回的点、终点加工为 元素的 d 属性,返回边在画布上渲染后的样式
Graph.registerConnector(
"algo-connector",
(s, e) => {
const offset = 4;
const deltaY = Math.abs(e.y - s.y);
const control = Math.floor((deltaY / 3) * 2);
const v1 = { x: s.x, y: s.y + offset + control };
const v2 = { x: e.x, y: e.y - offset - control };
return Path.normalize(
`M ${s.x} ${s.y} // 起始位置
L ${s.x} ${s.y + offset} // 到达位置
C ${v1.x} ${v1.y} ${v2.x} ${v2.y} ${e.x} ${e.y - offset} // 曲线到
L ${e.x} ${e.y} // 到达位置
`
);
},
true
);
const graph = new Graph({
container: container,
width: "100%",
height: "100%",
background: {
color: "#383838", opacity: "0.5"
},
grid: {
size: 10, // 网格大小 10px
visible: true, // 渲染网格背景
type: "mesh",
args: {
color: "#5B5B5B",
},
},
onPortRendered(args) { // 自定义连接桩
const selectors = args.contentSelectors
const container = selectors && selectors.foContent
const portAttr = args.port.attrs.data
if (container) {
const root = ReactDOM.createRoot(container);
root.render(
<Tooltip title={portAttr.type}>
<CustomizePort portAttr={portAttr}></CustomizePort> {/* eslint-disable-next-line */}
</Tooltip>,
);
}
},
history: true, // 撤销/重做,默认禁用
snapline: {
// 是否添加对齐线
enabled: true,
sharp: true,
},
// scroller: { // 画布是否可滚动
// enabled: true,
// pannable: true, // 是否启用画布平移能力
// autoResize: false, // 是否自动扩充/缩小画布
// },
mousewheel: {
// 是否可用鼠标绽放
enabled: true,
modifiers: ["ctrl", "meta"],
},
highlighting: {
// 触发某种交互时的高亮样式
magnetAdsorbed: {
// 连接桩可以被连接时高亮
name: "stroke",
args: {
attrs: {
fill: "#fff",
stroke: "#31d0c6",
strokeWidth: 4,
},
},
},
},
connecting: {
// 配置全局的连线规则
snap: true, // 是否自动吸附
allowMulti: true, // 是否允许在相同的起始节点和终止之间创建多条边
allowNode: false, // 是否允许边链接到节点(非节点上的链接桩)
allowBlank: false, // 是否允许连接到空白点
allowLoop: false, // 是否允许创建循环连线,即边的起始节点和终止节点为同一节点,
allowEdge: false, // 是否允许边链接到另一个边
highlight: true, // 拖动边时,是否高亮显示所有可用的连接桩或节点
connector: "algo-connector", // 边渲染到画布后的样式
connectionPoint: "anchor", // 指定连接点
anchor: "center", // 指定被连接的节点的锚点
// validateMagnet({ e, magnet, view, cell }) {
// // magnet 被按下时,是否创建新的边
// console.log("magent", e, magnet, view, cell);
// return false;
// },
createEdge() {
// 连接的过程中创建新的边
return graph.createEdge({
shape: "dag-edge",
attrs: {
line: {
strokeDasharray: "5 5",
},
},
zIndex: -1,
});
},
},
selecting: {
enabled: true,
className: 'my-selecting', // 附加样式名,用于定制样式,
multiple: true, // 是否启用点击多选,默认为 true。启用多选后按住 ctrl 或 command 键点击节点实现多选。
rubberband: false, // 是否启用框选
rubberNode: true, // 自定义框选节点
rubberEdge: true, // 自定义框选边
movable: true, // 在多选情况下,选中的节点是否一起移动
following: true, // 在多选情况下,选中的节点是否跟随鼠标实时移动
showNodeSelectionBox: true, // 是否显示节点的选择框
showEdgeSelectionBox: true, // 是否显示边的选择框
strict: true, // 启用框选时,选框完全包围节点时才选中节点,否则只需要选框与节点的包围盒(BBox)相交即可选中节点
content: (selection) => {
return StringExt.template(
'<%= length %> node<%= length > 1 ? "s":"" %> selected.',
)({ length: selection.length })
}
},
});
graph.enableHistory();
globalGraph = graph;
graph.on('blank:click', ({ e, x, y }) => {
console.log("fsfds")
setRightModelShow(false)
})
// eslint-disable-next-line
}, [container, CustomizePort]);