首发简书, 此为合并整理版.代码链接放在文末
В последнее время компании необходимо построить платформу машинного обучения для внутреннего использования, часть требований можно абстрагировать в направленный ациклический граф, а процесс НИОКР записывать пока ступаю на яму (фактически готовое колесо сильносцепленного бизнес не может быть найден 🤷), Если у вас есть подобные потребности, вы можете также замочить чашку годжи и медленно читать эту статью.
Содержание обучающей реализации:
Перетащите узел модели, установите взаимосвязь (соединение)
Внешние операции узла модели (добавление и удаление узла, обнаружение кольца DAG, реализованное внешним интерфейсом)
Плоскостное перемещение всего изображения модели (полный масштаб изображения, выделение, полный экран и т. д.)
Выбор технологии для фронтальной визуализации.
Для первоначальных требований к подключению рассмотрите возможность использования svg и canvas для комплексного достижения этого содержимого:
имя | svg | canvas |
---|---|---|
Качество изображения | Свободно масштабируйте вектор | Растровое изображение, искажение масштаба |
управляемый событиями | Основываясь на элементах dom, события привязки легко | Управляемая сценарием конфигурация событий не является гибкой |
представление | То же, что и выше, поэтому слишком много элементов рендеринга вызовет отставание. | Чрезвычайно высокая производительность и будущая тенденция закадрового холста |
Применимая сцена | Интерактивное поведение с меньшим количеством изображений на несколько порядков | Рендеринг супер повторяющихся элементов |
стоимость обучения | относительно простой | Есть стоимость, чтобы начать |
Поэтому svg используется как единое целое, и на рынке есть много готовых продуктов на основе svg, таких как ink knife, processon, noflo и множество платформ Али, которые неплохо себя показывают в некоторых сценариях (конечно, это также удобно открыть код обучения в любое время.напишите его~)
Книга продолжает предыдущую статью и возвращается к теме
Во-первых, реализация узлов
Соответствующий код узла (начальная версия){
name: "name1",
description: "description1",
id: 1,
parentNode: 0,
childNode: 2,
imgContent: "",
parentDetails: {
a: "",
b: ""
},
linkTo: [{ id: 2 }, { id: 3 }],
translate: {
left: 100,
top: 20
}
}
Более поздний этап (после шага 5 руководства) оптимизирован как:
{
name: "name2",
id: 2,
imgContent: "",
pos_x: 300,
pos_y: 400,
type: 'constant',
in_ports: [0, 1, 2, 3, 4],
out_ports: [0, 1, 2, 3, 4]
}
Пожалуйста, не обращайте внимания на абстракцию составителя душ, все управляется данными, а узлам модели нужно только взаимодействовать с серверными исследованиями и разработками, имитируя приведенную выше диаграмму.
Во-вторых, реализация модели узлового соединения
<path
class="connector"
v-for="(each, n) in item.linkTo" :key="n"
:d="computedLink(i, each, n)">
</path>
Основываясь на реализации vue, он напрямую используется: d Динамический расчет кривой Безье, Идея состоит в том, чтобы использовать идентификатор узлов входа и выхода для расчета начальной позиции и назначения формулы кривойНажмите -> о кривых Безье для справкиhttps://brucewar.gitbooks.io/svg-tutorial/15.SVG-path%E5%85%83%E7%B4%A0.html
В-третьих, реализация перетаскивания узла
dragPre(e, i) {
// 准备拖动节点
this.setInitRect(); // 初始化画板坐标
this.currentEvent = "dragPane"; // 修正行为
this.choice.index = i;
this.setDragFramePosition(e);
},
Причина инициализации артборда: Поскольку положение элемента в окне не фиксировано, каждый раз требуются начальные координаты, что удобно для расчета относительного смещения.
<g
:transform="`translate(${dragFrame.posX}, ${dragFrame.posY})`"
class="dragFrame">
<foreignObject width="180" height="30" >
<body xmlns="http://www.w3.org/1999/xhtml">
<div
v-show="currentEvent === 'dragPane'"
class="dragFrameArea">
</div>
</body>
</foreignObject>
</g>
mousedown时获取拖拽元素的下标,修正坐标
dragIng(e) {
if (this.currentEvent === "dragPane") {
this.setDragFramePosition(e);
// 模拟框随动
}
},
setDragFramePosition(e) {
const x = e.x - this.initPos.left; // 修正拖动元素坐标
const y = e.y - this.initPos.top;
this.dragFrame = { posX: x - 90, posY: y - 15 };
}
拖动时给模拟拖动的元素赋值位置
dragEnd(e) {
// 拖动结束
if (this.currentEvent === "dragPane") {
this.dragFrame = { dragFrame: false, posX: 0, posY: 0 };
this.setPanePosition(e); // 设定拖动后的位置
}
this.currentEvent = null; // 清空事件行为
},
setPanePosition(e) {
const x = e.x - this.initPos.left - 90;
const y = e.y - this.initPos.top - 15;
const i = this.choice.index;
this.DataAll[i].translate = { left: x, top: y };
},
拖动结束把新的位置赋值给对应元素,当然在实际项目中, 每次变更需要跟后台交互这些数据, 不需要前端模拟数据变更的,直接请求整张图的接口重新渲染就好了,更easy
В-четвертых, реализация перетаскивания узловых соединений
Как и в предыдущем шаге, мы также прослушиваем события mousedown, mousemove и mouseup, чтобы добиться эффекта перетаскивания соединения между узлами.Единственная трудность заключается в вычислении начальной позиции.
<g>
<path
class="connector"
:d="dragLinkPath()"
></path>
</g>
首先来个path
setInitRect() {
let { left, top } = document
.getElementById("svgContent")
.getBoundingClientRect();
this.initPos = { left, top }; // 修正坐标
},
linkPre(e, i) {
this.setInitRect();
this.currentEvent = "dragLink";
this.choice.index = i;
this.setDragLinkPostion(e, true);
e.preventDefault();
e.stopPropagation();
},
mousedown修正坐标
dragIng(e) {
if (this.currentEvent === "dragLink") {
this.setDragLinkPostion(e);
}
},
mousemove的时候确定位置
linkEnd(e, i) {
if (this.currentEvent === "dragLink") {
this.DataAll[this.choice.index].linkTo.push({ id: i });
this.DataAll.find(item => item.id === i).parentNode = 1;
}
this.currentEvent = null;
},
setDragLinkPostion(e, init) {
// 定位连线
const x = e.x - this.initPos.left;
const y = e.y - this.initPos.top;
if (init) {
this.dragLink = Object.assign({}, this.dragLink, {
fromX: x,
fromY: y
});
}
this.dragLink = Object.assign({}, this.dragLink, { toX: x, toY: y });
},
mouseup的时候判断连入了哪个元素
5. Объедините вышеуказанные шаги и разделите компоненты
По мере увеличения контента нам необходимо интегрировать весь контент и разделять компоненты на основе связанного контента.Подробности см. в структуре каталогов.
Все соединения становятся компонентами стрелки и наследуют только положение координат для рендеринга. симулироватьFrame и симулироватьАрроу только динамически наследуют координаты при перетаскивании, чтобы имитировать эффект перетаскивания6. Реализация перетаскивания и добавления узла
С процессно-ориентированной точки зрения перетаскивание узлов — это не что иное, как три операции:·Перед перетаскиванием判断当前情况下能否拖动, 拖动的元素携带的节点类型,节点名称等参数
·Тянуть模拟的节点随鼠标进行位移,将参数赋值给模拟的节点
· Перетаскивание判断松手位置是否在画板中, ( 更改模型数据 | 调用后台接口 )
Итак, нам нужен фиктивный элемент, который может перемещаться на весь экран.如图 class='nodesBus-contain'
<nodes-bus v-if="dragBus" :value="busValue.value" :pos_x="busValue.pos_x" :pos_y="busValue.pos_y" />
Этот элемент располагается рядом с самым большим контейнером в глобальной модели DOM и получает координаты положения и отображаемое имя.
dragBus: false,
busValue: {
value: "name",
pos_x: 100,
pos_y: 100
}
Самый внешний компонент использует dragBus для управления отображением, положением и т. д.
<div class="page-content" @mousedown="startNodesBus($event)" @mousemove="moveNodesBus($event)" @mouseup="endNodesBus($event)">
Внешний контейнер три события, mouseDown, mouseMove, mouseUp
<span @mousedown="dragIt('拖动1')">拖动我吧1</span>
<span @mousedown="dragIt('拖动2')">拖动我吧2</span>
dragIt(val) {
sessionStorage["dragDes"] = JSON.stringify({
drag: true,
name: val
});
}
Элементы, которые необходимо щелкнуть, чтобы вызвать перетаскивание, используют кеш для передачи данных и управления имитируемыми узлами.
startNodesBus(e) {
/**
* 别的组件调用时, 先放入缓存
* dragDes: {
* drag: true,
* name: 组件名称
* type: 组件类型
* model_id: 跟后台交互使用
* }
**/
let dragDes = null;
if (sessionStorage["dragDes"]) {
dragDes = JSON.parse(sessionStorage["dragDes"])
}
if (dragDes && dragDes.drag) {
const x = e.pageX;
const y = e.pageY;
this.busValue = Object.assign({}, this.busValue, {
pos_x: x,
pos_y: y,
value: dragDes.name
});
this.dragBus = true;
}
}
Когда он перемещается к верхнему компоненту, запускается событие mouseUp контейнера, отображается смоделированный узел и назначаются необходимые параметры.使用缓存来控制行为,是为了防止别的无关元素干扰.
moveNodesBus(e) {
if (this.dragBus) {
const x = e.pageX;
const y = e.pageY;
this.busValue = Object.assign({}, this.busValue, {
pos_x: x,
pos_y: y
});
}
},
Поведение перемещения очень простое, вам нужно только динамически назначать позицию мыши на странице для входа.
endNodesBus(e) {
let dragDes = null;
if (sessionStorage["dragDes"]) {
dragDes = JSON.parse(sessionStorage["dragDes"])
}
if (dragDes && dragDes.drag && e.toElement.id === "svgContent") {
const { model_id, type } = dragDes;
const pos_x = e.offsetX - 90; // 参数修正
const pos_y = e.offsetY - 15; // 参数修正
const params = {
model_id: sessionStorage["newGraph"],
desp: {
type,
pos_x,
pos_y,
name: this.busValue.value
}
};
this.addNode(params);
}
window.sessionStorage["dragDes"] = null;
this.dragBus = false;
}
Выньте положение мыши, когда mouseUp, и измените данные модели после исправления, Вызванный здесь this.addNode(params) исходит из vuex, и позже vuex будет объяснен в унифицированном виде.
7. Удаление узла
Удаление узла использует щелчок правой кнопкой мыши, чтобы открыть окно параметров, где мы можем отслеживать поведение элемента при щелчке правой кнопкой мыши и отключать все поведения по умолчанию. <g
v-for="(item, i) in DataAll.nodes"
:key="'_' + i" class="svgEach"
:transform="`translate(${item.pos_x}, ${item.pos_y})`"
@contextmenu="r_click_nodes($event, i)">
---------------------------------------------------------------------------
r_click_nodes(e, i) { // 节点的右键事件
this.setInitRect()
const id = this.DataAll.nodes[i].id;
const x = e.x - this.initPos.left;
const y = e.y - this.initPos.top;
this.is_edit_area = {
value: true,
x,
y,
id
}
e.stopPropagation();
e.cancelBubble = true;
e.preventDefault();
}
Затем передайте идентификатор узла и положение мыши операции компоненту имитации опции.nodesBus.vueчтобы убедиться, что поле параметра отображается в соответствующем положении. Здесь еще есть ямка.Нам нужно сделать так, чтобы нажатие на другие позиции могло закрыть модальное окно, поэтому нам нужно добавить слой маски.Здесь я пошел на хитрость и не добавил слой покрытия div
<foreignObject width="100%" height="100%" style="position: relative" @click="click_menu_cover($event)">
<body xmlns="http://www.w3.org/1999/xhtml" :style="get_menu_style()">
<div class="menu_contain">
<span @click="delEdges">删除节点</span>
<span>编辑</span>
<span>干点别的啥</span>
</div>
</body>
</foreignObject>
-------------------------------------------------
click_menu_cover(e) {
this.$emit('close_click_nodes')
e.preventDefault();
e.cancelBubble = true;
e.stopPropagation();
},
Просто перехватите mouseDown внутри компонента, чтобы закрыть всплывающее окно.
let params = {
model_id: sessionStorage['newGraph'],
id: this.isEditAreaShow.id
}
this.delNode(params)
model_id是本项目跟后台交互的参数请无视
Получите идентификатор и напрямую вызовите delNode vuex
Восемь, подключение, удаление узла и использование vuex
Чтобы разделить компоненты более подробно и облегчить обмен данными между компонентами, в качестве передачи данных этого проекта вводится vuex.Несколько компонентов используются вместе.dagStore.jsДанныеВсе,
addEdge: ({ commit }, { desp }) => { // 增加边
commit('ADD_EDGE_DATA', desp)
},
delEdge: ({ commit }, { id }) => { // 删除边
commit('DEL_EDGE_DATA', id)
},
moveNode: ({ commit }, params) => { // 移动点的位置
commit('MOVE_NODE_DATA', params)
},
addNode: ({ commit }, params) => { // 增加节点
commit('ADD_NODE_DATA', params)
},
delNode: ({ commit }, { id }) => { // 删除节点
commit('DEL_NODE_DATA', id)
},
Структура данных состояния
DataAll: {
nodes: [{
name: "name5",
id: 1,
imgContent: "",
pos_x: 100,
pos_y: 230,
type: "constant",
in_ports: [0, 1, 2],
out_ports: [0, 1, 2, 3, 4]
}],
edges: [{
id: 1,
dst_input_idx: 1,
dst_node_id: 1,
src_node_id: 2,
src_output_idx: 2
}],
model_id: 21
}
Все операции только изменяют DataAll в состоянии.
ADD_NODE_DATA: (state, params) => {
let _nodes = state.DataAll.nodes
_nodes.push({
...params.desp,
id: state.DataAll.nodes.length + 10,
in_ports: [0, 1, 2, 3, 4],
out_ports: [0, 1, 2, 3, 4]
})
}
новый узел
DEL_NODE_DATA: (state, id) => {
let _edges = []
let _nodes = []
state.DataAll.edges.forEach(item => {
if (item.dst_node_id !== id && item.src_node_id !== id) {
_edges.push(item)
}
})
state.DataAll.nodes.forEach(item => {
if (item.id !== id) {
_nodes.push(item)
}
})
state.DataAll.edges = _edges
state.DataAll.nodes = _nodes
}
Удаление узла
DEL_EDGE_DATA: (state, id) => {
let _edges = []
state.DataAll.edges.forEach((item, i) => {
if (item.id !== id) {
_edges.push(item)
}
})
state.DataAll.edges = _edges
},
Очистка соединений между узлами
ADD_EDGE_DATA: (state, desp) => {
let _DataAll = state.DataAll
_DataAll.edges.push({
...desp,
id: state.DataAll.edges.length + 10
})
/**
* 检测是否成环
**/
let isCircle = false
const { dst_node_id } = desp // 出口 入口id
const checkCircle = (dst_node_id, nth) => {
if (nth > _DataAll.nodes.length) {
isCircle = true
return false
} else {
_DataAll.edges.forEach(item => {
if (item.src_node_id === dst_node_id) {
console.log('目标节点是', item.src_node_id, '次数为', nth)
checkCircle(item.dst_node_id, ++nth)
}
})
}
}
checkCircle(dst_node_id, 1)
if (isCircle) {
_DataAll.edges.pop()
alert('禁止成环')
}
}
Приведенный выше код представляет собой увеличение узлов, что добавляет обнаружение того, является ли это кольцом, Продолжайте рекурсию узлов и найдите путь к узлу от целевого узла.Если количество циклов превышает общее количество узлов, это доказывает, что цикл есть, и операция отменяется.
在实际项目中, 每一步操作都可以传给后端,因此前端没有很大计算量,由后端同学负责放在缓存中计算
Девять, реализация перетаскивания всей картины
Реализация перетаскивания всего изображения Поместите все изображение в элемент ag внутри svg и динамически передайте перевод преобразования в элемент g, чтобы изменить положение.Поскольку это значение состояния компонента (состояние), автор не рекомендует помещать его в vuex для контроля, и рекомендуя положить в vue Данных в компоненте достаточно.В этом проекте автор сохраняет sessionStorage, что удобно для точного расчета текущего положения мыши и положения мыши в исходном масштабе. svgMouseDown(e) {
// svg鼠标按下触发事件分发
this.setInitRect();
if (this.currentEvent === "sel_area") {
this.selAreaStart(e);
} else {
// 那就拖动画布
this.currentEvent = "move_graph";
this.graphMovePre(e);
}
},
事件触发: 在svg画布mousedown的时候进行事件分发
/**
* 画布拖动
*/
graphMovePre(e) {
const { x, y } = e;
this.svg_trans_init = { x, y };
this.svg_trans_pre = { x: this.svg_left, y: this.svg_top };
},
graphMoveIng(e) {
const { x, y } = this.svg_trans_init;
this.svg_left = e.x - x + this.svg_trans_pre.x;
this.svg_top = e.y - y + this.svg_trans_pre.y;
sessionStorage["svg_left"] = this.svg_left;
sessionStorage["svg_top"] = this.svg_top;
},
在mousemove的过程中监听鼠标动态变化, 通过比较mousedown的初始位置,来更改当前画布位置
关于坐标计算的问题放在整图缩放里讲, 回归坐标计算需要考虑缩放倍数
10. Реализация масштабирования всего изображения и текущего положения мыши для расчета исходных координат.
То же, что и одиннадцать, с помощью transform: scale(x) тега g под svg для выполнения общего масштабирования узла.
<g :transform="` translate(${svg_left}, ${svg_top}) scale(${svgScale})`" >
Здесь svgScale использует vuex для управления, чтобы доказать, что не существует единой спецификации для управления состоянием компонентов, но все же настоятельно рекомендуется, чтобы состояние передавалось компонентам, а данные (данные) передавались vuex.
↓↓
svgScale: state => state.dagStore.svgSize
Здесь добавлен новый компонент плавающей панели, с которым удобно работать пользователям.
<template>
<g>
<foreignObject width="200px" height="30px" style="position: relative">
<body xmlns="http://www.w3.org/1999/xhtml">
<div class="control_menu">
<span @click="sizeExpend">╋</span>
<span @click="sizeShrink">一</span>
<span @click="sizeInit">╬</span>
<span :class="['sel_area', 'sel_area_ing'].indexOf(currentEvent) !== -1 ? 'sel_ing' : ''" @click="sel_area($event)">口</span>
<span @click="fullScreen">{{ changeScreen }}</span>
</div>
</body>
</foreignObject>
</g>
</template>
/**
* svg画板缩放行为
*/
sizeInit() {
this.changeSize("init"); // 回归到默认倍数
this.svg_left = 0; // 回归到默认位置
this.svg_top = 0;
sessionStorage['svg_left'] = 0;
sessionStorage['svg_top'] = 0;
},
sizeExpend() {
this.changeSize("expend"); // 画板放大0.1
},
sizeShrink() {
this.changeSize("shrink"); // 画板缩小0.1
},
Поскольку он управляется vuex, измените svgSize в мутации
CHANGE_SIZE: (state, action) => {
switch (action) {
case 'init':
state.svgSize = 1
break
case 'expend':
state.svgSize += 0.1
break
case 'shrink':
state.svgSize -= 0.1
break
default: state.svgSize = state.svgSize
}
sessionStorage['svgScale'] = state.svgSize
},
До настоящего времени мы завершили функции движущихся и масштабирования координат и масштабирования графика. Следующая важная проблема заключается в том, что, когда мы работаем поведение координат, мы можем получить только координаты в компоненте, что приведет к тому, что все результаты будут неуместны, Нам нужно пересчитать, чтобы вернуть реальные координаты без масштабирования и без смещения.
Возьмите конец перетаскивания узла в качестве примера
paneDragEnd(e) {
// 节点拖动结束
this.dragFrame = { dragFrame: false, posX: 0, posY: 0 }; // 关闭模态框
const x = // x轴坐标需要减去X轴位移量, 再除以放缩比例 减去模态框宽度一半
(e.x - this.initPos.left - (sessionStorage["svg_left"] || 0)) / this.svgScale -
90;
const y = // y轴坐标需要减去y轴位移量, 再除以放缩比例 减去模态框高度一半
(e.y - this.initPos.top - (sessionStorage["svg_top"] || 0)) / this.svgScale -
15;
let params = {
model_id: sessionStorage["newGraph"],
id: this.DataAll.nodes[this.choice.index].id,
pos_x: x,
pos_y: y
};
this.moveNode(params);
},
Для всех позиций, где используются координаты, необходимо вычесть смещение координат по горизонтали и вертикали, а затем разделить на коэффициент масштабирования, чтобы получить исходное соотношение. Код не будет повторяться.
Одиннадцать, полный экран
以chrome浏览器的为例, 不同浏览器都元素放缩有着不同的api
fullScreen() {
if (this.changeScreen === "全") {
this.changeScreen = "关";
let root = document.getElementById("svgContent");
root.webkitRequestFullScreen();
} else {
this.changeScreen = "全";
document.webkitExitFullscreen();
}
}
document.getElementById('svgContent').webkitRequestFullScreen() делает элемент полноэкранным. document.webkitExitFullScreen() выходит из полноэкранного режима.
12. Шатер с резинкой
Идея окна выбора резиновой ленты состоит в том, чтобы перетащить модальное поле div, получить координаты верхнего левого и нижнего правого и изменить состояние выбора узлов в пределах двух координат.
<div :class="choice.paneNode.indexOf(item.id) !== -1 ? 'pane-node-content selected' : 'pane-node-content'">
choice: {
paneNode: [], // 选取的节点下标组
index: -1,
point: -1 // 选取的点数的下标
},
Состояние выбора — это состояние компонента, поэтому оно находится под контролем компонента, без vuex.Выбору кадра нужно только поместить идентификатор выбранного элемента в панельNode.
selAreaStart(e) {
// 框选节点开始 在mousedown的时候调用
this.currentEvent = "sel_area_ing";
const x =
(e.x - this.initPos.left - (sessionStorage["svg_left"] || 0)) /
this.svgScale;
const y =
(e.y - this.initPos.top - (sessionStorage["svg_top"] || 0)) /
this.svgScale;
this.simulate_sel_area = {
left: x,
top: y,
width: 0,
height: 0
};
},
setSelAreaPostion(e) {
// 框选节点ing
const x =
(e.x - this.initPos.left - (sessionStorage["svg_left"] || 0)) /
this.svgScale;
const y =
(e.y - this.initPos.top - (sessionStorage["svg_top"] || 0)) /
this.svgScale;
const width = x - this.simulate_sel_area.left;
const height = y - this.simulate_sel_area.top;
this.simulate_sel_area.width = width;
this.simulate_sel_area.height = height;
},
getSelNodes(postions) {
// 选取框选的节点
const { left, top, width, height } = postions;
this.choice.paneNode.length = 0;
this.DataAll.nodes.forEach(item => {
if (
item.pos_x > left &&
item.pos_x < left + width &&
item.pos_y > top &&
item.pos_y < top + height
) {
this.choice.paneNode.push(item.id);
}
});
console.log("目前选择的节点是", this.choice.paneNode);
},
this.simulate_sel_area Поместите координаты начальной точки, высоту и ширину модального окна выбора кадра и передайте их компоненту для использования.
13. Организация мероприятия
До сих пор наш проект заполнен большим количеством событий, здесь мы можем управлять поведением события через currentEvent, запускать соответствующее событие путем прослушивания и распространять событие.
/**
* 事件分发器
*/
dragIng(e) {
// 事件发放器 根据currentEvent来执行系列事件
switch (this.currentEvent) {
case 'dragPane':
if (e.timeStamp - this.timeStamp > 200) {
this.currentEvent = "PaneDraging"; // 确认是拖动节点
};
break;
case 'PaneDraging':
this.setDragFramePosition(e); // 触发节点拖动
break;
case 'dragLink':
this.setDragLinkPostion(e); // 触发连线拖动
break;
case 'sel_area_ing':
this.setSelAreaPostion(e); // 触发框选
break;
case 'move_graph':
this.graphMoveIng(e);
break;
default: () => { }
}
}
Просмотрев весь контент, в общей сложности на реализацию требований к визуализации модели и разделение компонентов ушло три недели. Я надеюсь оказать простую помощь нуждающимся коллегам. Все коды не являются лучшими практиками, и я просто хочу бросать кирпичи и привлекать нефрита.
Конкретный код можно посмотреть на githubНажмите, чтобы прыгнуть: https://github.com/murongqimiao/DAGBoard.
или перейти кzhanglizhong.cnПосмотреть ДЕМО