Initial commit
This commit is contained in:
@@ -29,11 +29,11 @@ export const findOneTreeById = (id: number): Promise<BehaviorTreeDetailsResponse
|
|||||||
|
|
||||||
|
|
||||||
export const updateTree = (rt: Partial<BehaviorTree>): Promise<BasicResponse> => {
|
export const updateTree = (rt: Partial<BehaviorTree>): Promise<BasicResponse> => {
|
||||||
return req.postJson('/system/behaviortree/${id}', rt);
|
return req.postJson(`/system/behaviortree`, rt);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createTree = (rt: Partial<BehaviorTree>): Promise<BasicResponse> => {
|
export const createTree = (rt: Partial<BehaviorTree>): Promise<BasicResponse> => {
|
||||||
return req.putJson('/system/behaviortree/${id}', rt);
|
return req.putJson(`/system/behaviortree`, rt);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,20 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { NullableString } from '@/types';
|
import type { NullableString } from '@/types';
|
||||||
import type { NodeSetting } from './parameter';
|
import type { NodeSetting } from '@/views/decision/types';
|
||||||
|
|
||||||
|
export interface DraggableElement {
|
||||||
|
id: number | null,
|
||||||
|
key?: NullableString,
|
||||||
|
name: NullableString,
|
||||||
|
description: NullableString,
|
||||||
|
category: NullableString,
|
||||||
|
draggable: boolean,
|
||||||
|
parent?: DraggableElement,
|
||||||
|
children: DraggableElement[]
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export type ElementStatus = 'default' | 'success' | 'failed' | 'running' | string | null
|
export type ElementStatus = 'default' | 'success' | 'failed' | 'running' | string | null
|
||||||
|
|
||||||
@@ -51,6 +64,8 @@ export interface TaskNodeElement extends BaseElement {
|
|||||||
variables: ElementVariable[];
|
variables: ElementVariable[];
|
||||||
parameters: Record<any, any>;
|
parameters: Record<any, any>;
|
||||||
|
|
||||||
|
children?: TaskNodeElement[],
|
||||||
|
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7,9 +7,10 @@
|
|||||||
* that was distributed with this source code.
|
* that was distributed with this source code.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Clipboard, Edge, Graph, History, Keyboard, Path, Selection, Snapline, Transform } from '@antv/x6';
|
import { Edge, Graph, Path, Selection } from '@antv/x6';
|
||||||
import type { BaseElement } from '../types';
|
import type { ModelElement } from './element';
|
||||||
import type { Connecting } from '@antv/x6/lib/graph/options';
|
import type { Connecting } from '@antv/x6/lib/graph/options';
|
||||||
|
import {createLineOptions} from './line'
|
||||||
|
|
||||||
Graph.registerConnector(
|
Graph.registerConnector(
|
||||||
'sequenceFlowConnector',
|
'sequenceFlowConnector',
|
||||||
@@ -33,30 +34,30 @@ Graph.registerConnector(
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
export const createGraphConnectingAttributes = (): Partial<Connecting> => {
|
export const createGraphConnectingAttributes = (): Partial<Connecting> => {
|
||||||
|
const lineOptions = createLineOptions();
|
||||||
return {
|
return {
|
||||||
snap: true, // 当 snap 设置为 true 时连线的过程中距离节点或者连接桩 50px 时会触发自动吸附
|
snap: true, // 当 snap 设置为 true 时连线的过程中距离节点或者连接桩 50px 时会触发自动吸附
|
||||||
allowBlank: false, // 是否允许连接到画布空白位置的点,默认为 true
|
allowBlank: false, // 是否允许连接到画布空白位置的点,默认为 true
|
||||||
allowLoop: false, // 是否允许创建循环连线,即边的起始节点和终止节点为同一节点,默认为 true
|
allowLoop: false, // 是否允许创建循环连线,即边的起始节点和终止节点为同一节点,默认为 true
|
||||||
highlight: true, // 当连接到节点时,通过 sourceAnchor 来指定源节点的锚点。
|
highlight: true, // 当连接到节点时,通过 sourceAnchor 来指定源节点的锚点。
|
||||||
connector: 'sequenceFlowConnector',
|
connector: 'smooth',
|
||||||
connectionPoint: 'boundary', // 指定连接点,默认值为 boundary。
|
connectionPoint: 'anchor', // 指定连接点,默认值为 boundary。
|
||||||
anchor: 'center',
|
anchor: 'center',
|
||||||
|
|
||||||
router: 'manhattan',
|
|
||||||
// connector: {
|
|
||||||
// name: 'rounded',
|
|
||||||
// args: {
|
|
||||||
// radius: 8,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// connectionPoint: 'anchor',
|
|
||||||
|
|
||||||
// validateMagnet({ magnet }) {
|
// validateMagnet({ magnet }) {
|
||||||
// return magnet.getAttribute('port-group') !== 'top'
|
// return magnet.getAttribute('port-group') !== 'top'
|
||||||
// },
|
// },
|
||||||
// 验证连接
|
// 验证连接
|
||||||
|
createEdge(this: Graph) {
|
||||||
|
const edge: Edge = this.createEdge({
|
||||||
|
shape: 'edge',
|
||||||
|
...lineOptions, // 应用动画配置
|
||||||
|
attrs: lineOptions.attrs,
|
||||||
|
animation: lineOptions.animation,
|
||||||
|
markup: lineOptions.markup,
|
||||||
|
})
|
||||||
|
return edge;
|
||||||
|
},
|
||||||
validateConnection(this: Graph, { sourceCell, targetCell }) {
|
validateConnection(this: Graph, { sourceCell, targetCell }) {
|
||||||
console.error('validateConnection');
|
console.error('validateConnection');
|
||||||
if (!sourceCell || !targetCell) return false;
|
if (!sourceCell || !targetCell) return false;
|
||||||
@@ -67,21 +68,28 @@ export const createGraphConnectingAttributes = (): Partial<Connecting> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// const sourceData = sourceCell.getData() as GraphElement;
|
// const sourceData = sourceCell.getData() as GraphElement;
|
||||||
const targetData = targetCell.getData() as BaseElement;
|
const targetData = targetCell.getData() as ModelElement;
|
||||||
|
|
||||||
// 根节点不能作为子节点
|
// 根节点不能作为子节点
|
||||||
if (targetData.type === 'root') {
|
if (targetData.type === 'startEvent') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否已存在相同连接(保留原有逻辑)
|
// 4. 新增核心逻辑:检查源节点是否已有出边(已连接其他节点)
|
||||||
const edges: Edge[] = this.getEdges();
|
// const hasOutgoingEdge = this.getOutgoingEdges(sourceCell);
|
||||||
const existingConnection = edges.find(edge =>
|
// if (hasOutgoingEdge && hasOutgoingEdge.length > 1) {
|
||||||
edge.getSourceCell() === sourceCell &&
|
// return false;
|
||||||
edge.getTargetCell() === targetCell,
|
// }
|
||||||
);
|
|
||||||
|
|
||||||
return !existingConnection;
|
// 检查是否已存在相同连接
|
||||||
|
// const edges: Edge[] = this.getEdges();
|
||||||
|
// const existingConnection = edges.find(edge =>
|
||||||
|
// edge.getSourceCell() === sourceCell &&
|
||||||
|
// edge.getTargetCell() === targetCell,
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// return !existingConnection;
|
||||||
|
return true;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -103,10 +111,10 @@ export const createGraphCanvas = (container: HTMLDivElement, readonly: boolean =
|
|||||||
},
|
},
|
||||||
mousewheel: {
|
mousewheel: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
zoomAtMousePosition: true,
|
|
||||||
modifiers: 'ctrl',
|
modifiers: 'ctrl',
|
||||||
|
factor: 1.1,
|
||||||
|
maxScale: 1.5,
|
||||||
minScale: 0.5,
|
minScale: 0.5,
|
||||||
maxScale: 3,
|
|
||||||
},
|
},
|
||||||
highlighting: {
|
highlighting: {
|
||||||
magnetAdsorbed: {
|
magnetAdsorbed: {
|
||||||
@@ -162,16 +170,7 @@ export const createGraphCanvas = (container: HTMLDivElement, readonly: boolean =
|
|||||||
modifiers: 'shift',
|
modifiers: 'shift',
|
||||||
rubberband: true,
|
rubberband: true,
|
||||||
}),
|
}),
|
||||||
).use(
|
);
|
||||||
new Transform({
|
|
||||||
resizing: false,
|
|
||||||
rotating: false,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.use(new Snapline())
|
|
||||||
.use(new Keyboard())
|
|
||||||
.use(new Clipboard())
|
|
||||||
.use(new History());
|
|
||||||
|
|
||||||
return graph;
|
return graph;
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { computed, type ComputedRef, ref, type Ref } from 'vue';
|
import { computed, type ComputedRef, ref, type Ref } from 'vue';
|
||||||
import { type Dom, Graph } from '@antv/x6';
|
import { type Dom, Graph, Node } from '@antv/x6';
|
||||||
import type { NodeViewPositionEventArgs } from '@antv/x6/es/view/node/type';
|
import type { NodeViewPositionEventArgs } from '@antv/x6/es/view/node/type';
|
||||||
import { createGraphCanvas } from './graph';
|
import { createGraphCanvas } from './graph';
|
||||||
import { EventListener } from '@/utils/event';
|
import { EventListener } from '@/utils/event';
|
||||||
|
import type { ModelElement } from './element';
|
||||||
|
// import {createLineOptions} from './line'
|
||||||
|
|
||||||
export interface UseGraphCanvas {
|
export interface UseGraphCanvas {
|
||||||
container: Ref<HTMLDivElement | null>;
|
container: Ref<HTMLDivElement | null>;
|
||||||
@@ -85,7 +87,7 @@ export const useGraphCanvas = (readonly: boolean = false): UseGraphCanvas => {
|
|||||||
const cells = graph.value?.getSelectedCells();
|
const cells = graph.value?.getSelectedCells();
|
||||||
if (cells && cells.length) {
|
if (cells && cells.length) {
|
||||||
graph.value?.removeCells(cells);
|
graph.value?.removeCells(cells);
|
||||||
// 通知父节点更新状态
|
// 通知父组件更新状态
|
||||||
emitGraphEvent('cells:removed', cells);
|
emitGraphEvent('cells:removed', cells);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,7 +106,15 @@ export const useGraphCanvas = (readonly: boolean = false): UseGraphCanvas => {
|
|||||||
|
|
||||||
// 监听连线删除
|
// 监听连线删除
|
||||||
graph.value.on('edge:removed', ({ edge }) => {
|
graph.value.on('edge:removed', ({ edge }) => {
|
||||||
emitGraphEvent('cells:removed', edge); // 添加此行
|
emitGraphEvent('edge:removed', edge); // 添加此行
|
||||||
|
});
|
||||||
|
|
||||||
|
graph.value.on('edge:added', ({ edge }) => {
|
||||||
|
// const lineOptions = createLineOptions();
|
||||||
|
// edge.setAttrs(lineOptions.attrs);
|
||||||
|
// edge.set(lineOptions.animation);
|
||||||
|
// edge.setMarkup(lineOptions.markup);
|
||||||
|
emitGraphEvent('edge:added', edge); // 添加此行
|
||||||
});
|
});
|
||||||
|
|
||||||
graph.value.on('edge:contextmenu', (ctx) => {
|
graph.value.on('edge:contextmenu', (ctx) => {
|
||||||
@@ -132,28 +142,28 @@ export const useGraphCanvas = (readonly: boolean = false): UseGraphCanvas => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
graph.value.on('edge:connected', ({ edge }) => {
|
graph.value.on('edge:connected', ({ edge }) => {
|
||||||
// const sourceNode = edge.getSourceCell() as Node;
|
const sourceNode = edge.getSourceCell() as Node;
|
||||||
// const targetNode = edge.getTargetCell() as Node;
|
const targetNode = edge.getTargetCell() as Node;
|
||||||
|
|
||||||
// if (sourceNode && targetNode) {
|
if (sourceNode && targetNode) {
|
||||||
// const sourceData = sourceNode.getData() as TaskNodeElement;
|
const sourceData = sourceNode.getData() as ModelElement;
|
||||||
// const targetData = targetNode.getData() as TaskNodeElement;
|
const targetData = targetNode.getData() as ModelElement;
|
||||||
//
|
|
||||||
// // 将连线存储到节点数据中
|
// 将连线存储到节点数据中
|
||||||
// const sourceEdges = sourceData.edges || [];
|
const sourceEdges = sourceData.edges || [];
|
||||||
// const existingEdge = sourceEdges.find(e => e.targetKey === targetNode.id);
|
const existingEdge = sourceEdges.find(e => e.targetKey === targetNode.id);
|
||||||
//
|
|
||||||
// if (!existingEdge) {
|
if (!existingEdge) {
|
||||||
// sourceEdges.push({
|
sourceEdges.push({
|
||||||
// key: edge.id,
|
key: edge.id,
|
||||||
// sourceKey: sourceNode.id,
|
sourceKey: sourceNode.id,
|
||||||
// sourceName: sourceData.name,
|
sourceName: sourceData.name,
|
||||||
// targetKey: targetNode.id,
|
targetKey: targetNode.id,
|
||||||
// targetName: targetData.name,
|
targetName: targetData.name,
|
||||||
// });
|
});
|
||||||
// sourceNode.setData({ ...sourceData, edges: sourceEdges });
|
sourceNode.replaceData({ ...sourceData, edges: sourceEdges });
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
edge.attr('line/stroke', '#3b82f6'); // 显式设置颜色
|
edge.attr('line/stroke', '#3b82f6'); // 显式设置颜色
|
||||||
edge.attr('line/strokeWidth', 2); // 显式设置宽度
|
edge.attr('line/strokeWidth', 2); // 显式设置宽度
|
||||||
|
|||||||
47
modeler/src/views/decision/builder/line.ts
Normal file
47
modeler/src/views/decision/builder/line.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of the kernelstudio package.
|
||||||
|
*
|
||||||
|
* (c) 2014-2026 zlin <admin@kernelstudio.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE file
|
||||||
|
* that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Shape } from '@antv/x6';
|
||||||
|
|
||||||
|
export const createLineOptions = (): any => {
|
||||||
|
return {
|
||||||
|
markup: [
|
||||||
|
{
|
||||||
|
tagName: 'circle',
|
||||||
|
selector: 'marker',
|
||||||
|
attrs: {
|
||||||
|
stroke: 'none',
|
||||||
|
r: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...Shape.Edge.getMarkup() as any,
|
||||||
|
],
|
||||||
|
attrs: {
|
||||||
|
line: {
|
||||||
|
stroke: '#5da0df',
|
||||||
|
strokeWidth: 2,
|
||||||
|
strokeDasharray: ' ',
|
||||||
|
strokeDashoffset: 0,
|
||||||
|
},
|
||||||
|
marker: {
|
||||||
|
fill: '#5da0df',
|
||||||
|
atConnectionRatio: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: [
|
||||||
|
[
|
||||||
|
{ 'attrs/marker/atConnectionRatio': 1 },
|
||||||
|
{
|
||||||
|
duration: 2000,
|
||||||
|
iterations: Infinity,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,74 @@
|
|||||||
<template>
|
<template>
|
||||||
<a-dropdown :trigger="['contextmenu']" @openChange="handleVisibleChange">
|
<a-dropdown :trigger="['contextmenu']" @openChange="handleVisibleChange">
|
||||||
<a-card :class="['ks-designer-node', `ks-designer-node-${element?.type}`]" hoverable>
|
<a-card
|
||||||
|
:class="[
|
||||||
|
'ks-designer-node',
|
||||||
|
`ks-designer-${element?.category ?? 'model'}-node`
|
||||||
|
]"
|
||||||
|
hoverable
|
||||||
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
{{ element?.name ?? '-' }}
|
<a-space>
|
||||||
|
<span class="ks-designer-node-icon"></span>
|
||||||
|
<span class="ks-designer-node-title">{{ element?.name ?? '-' }}</span>
|
||||||
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- 节点内容区域 -->
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<p>{{ element?.description ?? '-' }}</p>
|
<div class="ks-designer-node-content">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in element?.element?.children || []"
|
||||||
|
:key="item.id || index"
|
||||||
|
class="ks-designer-node-row"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
:data-port="`in-${item.id || index}`"
|
||||||
|
:title="`入桩: ${item.name}`"
|
||||||
|
class="port port-in"
|
||||||
|
magnet="passive"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<!-- child名称 -->
|
||||||
|
<div class="ks-designer-node-name">
|
||||||
|
{{ item.name }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧出桩:只能作为连线源 -->
|
||||||
|
<div
|
||||||
|
:data-port="`out-${item.id || index}`"
|
||||||
|
:title="`出桩: ${item.name}`"
|
||||||
|
class="port port-out"
|
||||||
|
magnet="active"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!(element?.element?.children && element?.element?.children?.length > 0)" class="ks-designer-node-row">
|
||||||
|
<div class="port port-in" data-port="in-0" magnet="passive"></div>
|
||||||
|
<div class="ks-designer-node-name" v-if="element?.category !== 'component'">
|
||||||
|
{{ element?.name ?? '-' }}
|
||||||
|
</div>
|
||||||
|
<div class="ks-designer-node-name" v-else>
|
||||||
|
<p>隐藏纬度: {{ element?.parameters?.hiddenLatitude ?? '-' }}</p>
|
||||||
|
<p>激活函数: {{ element?.parameters?.activationFunction ?? '-' }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="port port-out" data-port="out-0" magnet="active"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <div class="w-full" v-else>-->
|
||||||
|
<!-- <div class="ks-designer-node-content">-->
|
||||||
|
<!-- <div class="port port-in" data-port="in-0" magnet="passive"></div>-->
|
||||||
|
<!-- <div class="ks-designer-node-name">-->
|
||||||
|
<!-- <p>隐藏纬度: {{ element?.parameters?.hiddenLatitude ?? '-' }}</p>-->
|
||||||
|
<!-- <p>激活函数: {{ element?.parameters?.activationFunction ?? '-' }}</p>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<!-- <div class="port port-out" data-port="out-0" magnet="active"></div>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<!-- </div>-->
|
||||||
</a-card>
|
</a-card>
|
||||||
|
|
||||||
<template #overlay>
|
<template #overlay>
|
||||||
<a-menu @click="handleMenuClick">
|
<a-menu @click="handleMenuClick">
|
||||||
<a-menu-item key="delete">
|
<a-menu-item key="delete">
|
||||||
@@ -24,37 +85,36 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, onMounted, onUnmounted, ref } from 'vue';
|
import { defineComponent, onMounted, onUnmounted, ref } from 'vue';
|
||||||
import { elementProps } from './props';
|
import { elementProps } from './props';
|
||||||
import type { SettingTaskNodeElement } from '../types';
|
import type { ModelElement } from './element';
|
||||||
import { DeleteOutlined, SettingOutlined } from '@ant-design/icons-vue';
|
import { DeleteOutlined, SettingOutlined } from '@ant-design/icons-vue';
|
||||||
import type { Graph } from '@antv/x6';
|
import type { Graph } from '@antv/x6';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'SettingTaskNodeElement',
|
name: 'ModelElement',
|
||||||
components: {
|
components: {
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
},
|
},
|
||||||
props: elementProps,
|
props: elementProps,
|
||||||
setup(_props) {
|
setup(_props) {
|
||||||
// 初始化 element,保证类型是 SettingTaskNodeElement 或 null
|
const element = ref<ModelElement | null>(
|
||||||
const element = ref<SettingTaskNodeElement | null>(
|
_props.node ? (_props.node.getData() as ModelElement) : null,
|
||||||
_props.node ? (_props.node.getData() as SettingTaskNodeElement) : null,
|
|
||||||
);
|
);
|
||||||
const updateKey = ref(0);
|
const updateKey = ref(0);
|
||||||
const isMenuVisible = ref(false);
|
const isMenuVisible = ref(false);
|
||||||
|
|
||||||
// 获取节点所在的画布实例
|
// 获取画布实例
|
||||||
const getGraph = (): Graph | null => {
|
const getGraph = (): Graph | null => {
|
||||||
return _props.graph as Graph || null;
|
return _props.graph as Graph || null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 监听节点数据变化
|
||||||
const handleDataChange = () => {
|
const handleDataChange = () => {
|
||||||
if (_props.node) {
|
if (_props.node) {
|
||||||
element.value = _props.node.getData() as SettingTaskNodeElement;
|
element.value = _props.node.getData() as ModelElement;
|
||||||
} else {
|
} else {
|
||||||
element.value = null;
|
element.value = null;
|
||||||
}
|
}
|
||||||
console.info('handleDataChange', element.value);
|
|
||||||
updateKey.value++;
|
updateKey.value++;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -74,36 +134,24 @@ export default defineComponent({
|
|||||||
const graph = getGraph();
|
const graph = getGraph();
|
||||||
if (graph) {
|
if (graph) {
|
||||||
try {
|
try {
|
||||||
// 获取与该节点关联的所有边
|
// 先删除关联边
|
||||||
const connectedEdges = graph.getConnectedEdges(_props.node);
|
const connectedEdges = graph.getConnectedEdges(_props.node);
|
||||||
|
connectedEdges.forEach(edge => graph.removeEdge(edge));
|
||||||
// 先移除关联的边
|
// 再删除节点
|
||||||
connectedEdges.forEach(edge => {
|
|
||||||
graph.removeEdge(edge);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 再移除节点本身
|
|
||||||
graph.removeNode(_props.node);
|
graph.removeNode(_props.node);
|
||||||
|
|
||||||
console.info(`节点 ${_props.node.id} 已删除`);
|
console.info(`节点 ${_props.node.id} 已删除`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('删除节点失败:', error);
|
console.error('删除节点失败:', error);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
console.error('无法获取 Graph 实例');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭右键菜单
|
|
||||||
isMenuVisible.value = false;
|
isMenuVisible.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
console.info('node onMounted')
|
|
||||||
_props.node?.on('change:data', handleDataChange);
|
_props.node?.on('change:data', handleDataChange);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
console.info('node onUnmounted')
|
|
||||||
_props.node?.off('change:data', handleDataChange);
|
_props.node?.off('change:data', handleDataChange);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -117,71 +165,168 @@ export default defineComponent({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
.x6-widget-selection-box {
|
|
||||||
border: 1px dashed #7a6986;
|
|
||||||
box-shadow: 2px 2px 5px #000000;
|
|
||||||
}
|
|
||||||
.ks-designer-node {
|
.ks-designer-node {
|
||||||
background: #1b3875;
|
background: linear-gradient(150deg, #093866 1%, #1f69b3 55%);
|
||||||
//background: url('@/assets/icons/bg-node.png') center / 100% 100%;
|
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 2px;
|
border-radius: 8px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
&:hover {
|
|
||||||
box-shadow: 0 1px 2px -2px rgb(0 0 0), 0 3px 6px 0 rgb(0 0 0 / 60%), 0 5px 12px 4px rgb(0 0 0 / 30%);
|
|
||||||
}
|
|
||||||
.ant-card-head {
|
.ant-card-head {
|
||||||
border: 0;
|
border: 0;
|
||||||
height: 30px;
|
height: 38px;
|
||||||
min-height: 30px;
|
min-height: 38px;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
color: #ddd;
|
color: #ddd;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
padding: 0 15px;
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ks-designer-node-icon {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
left: 8px;
|
||||||
|
top: 13px;
|
||||||
|
background: url('@/assets/icons/model-4.svg') center / 100% 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ks-designer-node-title {
|
||||||
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-card-body {
|
.ant-card-body {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
height: calc(100% - 30px);
|
height: calc(100% - 38px);
|
||||||
background: #24417e;
|
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
padding: 15px !important;
|
padding: 8px 15px;
|
||||||
//overflow: hidden;
|
overflow-y: auto;
|
||||||
//white-space: nowrap;
|
border-top: 1px solid #195693;
|
||||||
//text-overflow: ellipsis;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//&.ks-designer-node-root{
|
&.ks-designer-task-node {
|
||||||
//background: #645525;
|
background: linear-gradient(150deg, #20421b 1%, #4a6646 55%);
|
||||||
//.ant-card-body{
|
|
||||||
// background: #726334;
|
.ant-card-body {
|
||||||
//}
|
border-top: 1px solid #466741;
|
||||||
//}
|
}
|
||||||
&.ks-designer-node-select {
|
|
||||||
background: #255464;
|
.ks-designer-node-icon {
|
||||||
.ant-card-body{
|
background: url('@/assets/icons/m-02.png') center / 100% 100%;
|
||||||
background: #1c4654;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.ks-designer-node-precondition,
|
&.ks-designer-input-node {
|
||||||
&.ks-designer-node-parallel,
|
background: linear-gradient(150deg, #083058 1%, #1e5d9b 55%);
|
||||||
&.ks-designer-node-sequence{
|
|
||||||
background: #4c5a9d;
|
.ant-card-body {
|
||||||
.ant-card-body{
|
border-top: 1px solid #105ca7;
|
||||||
background: #3f4d8d;
|
}
|
||||||
|
|
||||||
|
.ks-designer-node-icon {
|
||||||
|
background: url('@/assets/icons/icon-model-input.png') center / 100% 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.ks-designer-node-action{
|
|
||||||
background: #645525;
|
&.ks-designer-action-node {
|
||||||
.ant-card-body{
|
background: linear-gradient(150deg, #343207 1%, #485010 55%);
|
||||||
background: #726334;
|
|
||||||
|
.ant-card-body {
|
||||||
|
border-top: 1px solid #59550e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ks-designer-node-icon {
|
||||||
|
background: url('@/assets/icons/bg-fk-point.png') center / 100% 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.ks-designer-component-node {
|
||||||
|
background: linear-gradient(150deg, #06226b 1%, #1a43a7 55%);
|
||||||
|
|
||||||
|
.ant-card-body {
|
||||||
|
border-top: 1px solid #26448c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ks-designer-control-node {
|
||||||
|
background: linear-gradient(150deg, #1d4f32 1%, #326a5d 55%);
|
||||||
|
|
||||||
|
.ant-card-body {
|
||||||
|
border-top: 1px solid #326a5d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ks-designer-node-icon {
|
||||||
|
background: url('@/assets/icons/bg-model-builder-card-title.png') center / 100% 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 连接桩容器样式
|
||||||
|
.ks-designer-node-content {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px; // 每个child行之间的间距
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每个child行(包含左右桩+文本)
|
||||||
|
.ks-designer-node-row {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
min-height: 24px; // 固定行高,保证桩对齐
|
||||||
|
}
|
||||||
|
|
||||||
|
// 连接桩基础样式
|
||||||
|
.port {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: crosshair;
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-shadow: 0 0 0 2px rgb(74 114 214 / 80%);
|
||||||
|
z-index: 10; // 确保桩在最上层
|
||||||
|
// X6 标记为可连线的磁体
|
||||||
|
magnet: true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 左侧入桩样式
|
||||||
|
.port-in {
|
||||||
|
background-color: #093866; // 青色:入桩
|
||||||
|
margin-right: 8px; // 与文本的间距
|
||||||
|
//border: 1px solid #093866;
|
||||||
|
// X6 只能作为连线目标(入)
|
||||||
|
magnet: passive;
|
||||||
|
box-shadow: none;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: block;
|
||||||
|
background: url('@/assets/icons/point.svg') center / 100% 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 右侧出桩样式
|
||||||
|
.port-out {
|
||||||
|
margin-left: 8px; // 与文本的间距
|
||||||
|
margin-right: 5px;
|
||||||
|
// X6 只能作为连线源(出)
|
||||||
|
magnet: active;
|
||||||
|
box-shadow: none;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: block;
|
||||||
|
background: url('@/assets/icons/arrow-right.svg') center / 100% 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 节点文本样式
|
||||||
|
.ks-designer-node-name {
|
||||||
|
flex: 1; // 占满中间空间
|
||||||
|
line-height: 24px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
@@ -12,11 +12,11 @@ export const createPort = (name: string = 'top', args: Record<any, any> = { dx:
|
|||||||
position: { name: name, args: args },
|
position: { name: name, args: args },
|
||||||
attrs: {
|
attrs: {
|
||||||
circle: {
|
circle: {
|
||||||
r: 3, // 大小
|
r: 4, // 大小
|
||||||
magnet: true,
|
magnet: true,
|
||||||
stroke: '#999', // 边框颜色
|
stroke: '#1b5e9f', // 边框颜色
|
||||||
strokeWidth: 1, // 边框大小
|
strokeWidth: 1, // 边框大小
|
||||||
fill: '#ffffff', // 填充颜色
|
fill: '#3578bf', // 填充颜色
|
||||||
style: {
|
style: {
|
||||||
visibility: 'visible', // 是否可见
|
visibility: 'visible', // 是否可见
|
||||||
},
|
},
|
||||||
@@ -25,19 +25,27 @@ export const createPort = (name: string = 'top', args: Record<any, any> = { dx:
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createPorts = () => {
|
export const createPorts = (top: boolean = true, right: boolean = true, bottom: boolean = true, left: boolean = true) => {
|
||||||
|
const groups: any = {};
|
||||||
|
const items: any = [];
|
||||||
|
if (top) {
|
||||||
|
groups['top'] = createPort('top');
|
||||||
|
items.push({ group: 'top', id: 'top' });
|
||||||
|
}
|
||||||
|
if (right) {
|
||||||
|
groups['right'] = createPort('right');
|
||||||
|
items.push({ group: 'right', id: 'right' });
|
||||||
|
}
|
||||||
|
if (bottom) {
|
||||||
|
groups['bottom'] = createPort('bottom');
|
||||||
|
items.push({ group: 'bottom', id: 'bottom' });
|
||||||
|
}
|
||||||
|
if (left) {
|
||||||
|
groups['left'] = createPort('left');
|
||||||
|
items.push({ group: 'left', id: 'left' });
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
groups: {
|
groups: groups,
|
||||||
top: createPort('top'),
|
items: items,
|
||||||
right: createPort('right'),
|
|
||||||
bottom: createPort('bottom'),
|
|
||||||
left: createPort('left'),
|
|
||||||
},
|
|
||||||
items: [
|
|
||||||
{ group: 'top', id: 'top' },
|
|
||||||
{ group: 'right', id: 'right' },
|
|
||||||
{ group: 'bottom', id: 'bottom' },
|
|
||||||
{ group: 'left', id: 'left' },
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
import { Graph, Node } from '@antv/x6';
|
import { Graph, Node } from '@antv/x6';
|
||||||
import { type ExtractPropTypes, type PropType } from 'vue';
|
import { type ExtractPropTypes, type PropType } from 'vue';
|
||||||
import type { BaseElement } from '../types';
|
import type { ModelElement } from './element';
|
||||||
|
|
||||||
export const elementProps = {
|
export const elementProps = {
|
||||||
node: {
|
node: {
|
||||||
@@ -21,7 +21,7 @@ export const elementProps = {
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
element: {
|
element: {
|
||||||
type: Object as PropType<BaseElement>,
|
type: Object as PropType<ModelElement>,
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,14 +7,21 @@
|
|||||||
* that was distributed with this source code.
|
* that was distributed with this source code.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the kernelstudio package.
|
||||||
|
*
|
||||||
|
* (c) 2014-2025 zlin <admin@kernelstudio.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE file
|
||||||
|
* that was distributed with this source code.
|
||||||
|
*/
|
||||||
import { register } from '@antv/x6-vue-shape';
|
import { register } from '@antv/x6-vue-shape';
|
||||||
import ModelElement from './node.vue';
|
import ModelElement from './node.vue';
|
||||||
import { createPorts } from './ports';
|
|
||||||
|
|
||||||
export const registerNodeElement = (type: string = 'task') => {
|
export const registerNodeElement = () => {
|
||||||
console.info('registerNodeElement');
|
console.info('registerNodeElement');
|
||||||
register({
|
register({
|
||||||
shape: type,
|
shape: 'node',
|
||||||
component: ModelElement,
|
component: ModelElement,
|
||||||
width: 120,
|
width: 120,
|
||||||
attrs: {
|
attrs: {
|
||||||
@@ -29,6 +36,14 @@ export const registerNodeElement = (type: string = 'task') => {
|
|||||||
dragging: {
|
dragging: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
ports: createPorts(),
|
// 配置端口识别规则,
|
||||||
|
portMarkup: [
|
||||||
|
{
|
||||||
|
tagName: 'div',
|
||||||
|
selector: 'port-body',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// 告诉 X6 如何识别 Vue 组件内的端口
|
||||||
|
portAttribute: 'data-port',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
109
modeler/src/views/decision/builder/utils.ts
Normal file
109
modeler/src/views/decision/builder/utils.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of the kernelstudio package.
|
||||||
|
*
|
||||||
|
* (c) 2014-2026 zlin <admin@kernelstudio.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE file
|
||||||
|
* that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
import type { EdgeNodeElement, NodeGraph, TaskNodeElement } from './element';
|
||||||
|
import { Edge, Graph, Node } from '@antv/x6';
|
||||||
|
|
||||||
|
export const defaultHeight: Record<string, number> = {
|
||||||
|
component: 110,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createModelNode = (element: TaskNodeElement, width: number = 250, height: number = 120): any => {
|
||||||
|
let realHeight = defaultHeight[element.category as string];
|
||||||
|
if (!realHeight) {
|
||||||
|
realHeight = 120;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
shape: 'node',
|
||||||
|
id: element.key,
|
||||||
|
position: {
|
||||||
|
x: element.position?.x || 0,
|
||||||
|
y: element.position?.y || 0,
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
width: width,
|
||||||
|
height: realHeight,
|
||||||
|
},
|
||||||
|
attrs: {
|
||||||
|
label: {
|
||||||
|
text: element.name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: element,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resolveNodeTaskElements = (graph: Graph): TaskNodeElement[] => {
|
||||||
|
const taskElements: TaskNodeElement[] = [];
|
||||||
|
if (graph) {
|
||||||
|
const nodes = graph?.getNodes() as Node[];
|
||||||
|
if (nodes) {
|
||||||
|
nodes.forEach(node => {
|
||||||
|
const nodeData = node.getData() as TaskNodeElement;
|
||||||
|
const newElement = {
|
||||||
|
...nodeData,
|
||||||
|
key: node.id,
|
||||||
|
position: node.getPosition(),
|
||||||
|
width: node.getSize().width,
|
||||||
|
height: node.getSize().height,
|
||||||
|
};
|
||||||
|
taskElements.push(newElement);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return taskElements;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resolveNodeEdgeElements = (graph: Graph): EdgeNodeElement[] => {
|
||||||
|
const edgeElements: EdgeNodeElement[] = [];
|
||||||
|
if (graph) {
|
||||||
|
const graphEdges = graph?.getEdges() ?? [] as Edge[];
|
||||||
|
if (graphEdges) {
|
||||||
|
graphEdges.forEach(edge => {
|
||||||
|
const nodeData = edge.getData() as TaskNodeElement;
|
||||||
|
edgeElements.push({
|
||||||
|
id: nodeData?.id ?? 0,
|
||||||
|
key: edge.id,
|
||||||
|
type: 'edge',
|
||||||
|
status: nodeData?.status,
|
||||||
|
source: edge.getSource() ? edge.getSource() as unknown as string : null,
|
||||||
|
target: edge.getSource() ? edge.getTarget() as unknown as string : null,
|
||||||
|
attrs: edge.getAttrs() ?? {},
|
||||||
|
router: edge.getRouter() ?? {},
|
||||||
|
connector: edge.getConnector() ?? null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return edgeElements;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resolveNodeGraph = (graph: Graph): NodeGraph => {
|
||||||
|
const nodes: TaskNodeElement[] = resolveNodeTaskElements(graph);
|
||||||
|
const edges: EdgeNodeElement[] = resolveNodeEdgeElements(graph);
|
||||||
|
return {
|
||||||
|
nodes,
|
||||||
|
edges,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hasElements = (graph: Graph): boolean => {
|
||||||
|
if (graph) {
|
||||||
|
const taskElements: TaskNodeElement[] = resolveNodeTaskElements(graph);
|
||||||
|
return taskElements.length > 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hasRootElementNode = (graph: Graph): boolean => {
|
||||||
|
if (graph) {
|
||||||
|
const taskElements: TaskNodeElement[] = resolveNodeTaskElements(graph);
|
||||||
|
return taskElements.filter(e => e.type === 'root').length === 1;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
@@ -68,11 +68,14 @@ import Header from './header.vue';
|
|||||||
import Properties from './properties.vue';
|
import Properties from './properties.vue';
|
||||||
import { useGraphCanvas } from './builder/hooks';
|
import { useGraphCanvas } from './builder/hooks';
|
||||||
import { registerNodeElement } from './builder/register';
|
import { registerNodeElement } from './builder/register';
|
||||||
import type { BehaviorTree, EdgeNodeElement, NodeGraph, NodeTemplate, SettingTaskNodeElement } from './types';
|
import type { BehaviorTree, NodeTemplate } from './types';
|
||||||
|
import type { NodeGraph, SettingTaskNodeElement, TaskNodeElement } from './builder/element';
|
||||||
import { createTree, findOneTreeById, updateTree } from './api';
|
import { createTree, findOneTreeById, updateTree } from './api';
|
||||||
import { createTaskNodeElement, createTaskNodeElementFromTemplate, hasElements, hasRootElementNode, resolveNodeGraph } from './utils/node';
|
import { createModelNode, hasElements, hasRootElementNode, resolveNodeGraph } from './builder/utils';
|
||||||
|
import { createTaskNodeElementFromTemplate } from './utils/node';
|
||||||
import TressCard from './trees-card.vue';
|
import TressCard from './trees-card.vue';
|
||||||
import NodesCard from './nodes-card.vue';
|
import NodesCard from './nodes-card.vue';
|
||||||
|
import { createLineOptions } from '@/views/decision/builder/line.ts';
|
||||||
|
|
||||||
const TeleportContainer = defineComponent(getTeleport());
|
const TeleportContainer = defineComponent(getTeleport());
|
||||||
|
|
||||||
@@ -187,7 +190,7 @@ export default defineComponent({
|
|||||||
// 创建节点数据
|
// 创建节点数据
|
||||||
const settingTaskElement: SettingTaskNodeElement = createTaskNodeElementFromTemplate(template, { x, y });
|
const settingTaskElement: SettingTaskNodeElement = createTaskNodeElementFromTemplate(template, { x, y });
|
||||||
// 创建节点
|
// 创建节点
|
||||||
const settingTaskNode = createTaskNodeElement(settingTaskElement);
|
const settingTaskNode = createModelNode(settingTaskElement);
|
||||||
console.info('create settingTaskNode: ', settingTaskElement, settingTaskNode);
|
console.info('create settingTaskNode: ', settingTaskElement, settingTaskNode);
|
||||||
|
|
||||||
// 将节点添加到画布
|
// 将节点添加到画布
|
||||||
@@ -205,12 +208,23 @@ export default defineComponent({
|
|||||||
console.error('handleSelectTree', tree);
|
console.error('handleSelectTree', tree);
|
||||||
findOneTreeById(tree.id).then(r => {
|
findOneTreeById(tree.id).then(r => {
|
||||||
if (r.data) {
|
if (r.data) {
|
||||||
currentBehaviorTree.value = r.data;
|
let nodeGraph: NodeGraph | null = null;
|
||||||
try {
|
try {
|
||||||
currentBehaviorTree.value.graph = JSON.parse(r.data?.xmlContent as unknown as string) as unknown as NodeGraph;
|
nodeGraph = JSON.parse(r.data?.xmlContent as unknown as string) as unknown as NodeGraph;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('parse error,cause:', e);
|
console.error('parse error,cause:', e);
|
||||||
}
|
}
|
||||||
|
if (!nodeGraph) {
|
||||||
|
nodeGraph = {
|
||||||
|
nodes: [],
|
||||||
|
edges: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
currentBehaviorTree.value = {
|
||||||
|
...r.data,
|
||||||
|
graph: nodeGraph,
|
||||||
|
};
|
||||||
|
console.error(currentBehaviorTree.value);
|
||||||
createElements();
|
createElements();
|
||||||
} else {
|
} else {
|
||||||
message.error(r.msg ?? '行为树不存在.');
|
message.error(r.msg ?? '行为树不存在.');
|
||||||
@@ -219,32 +233,30 @@ export default defineComponent({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const createElements = () => {
|
const createElements = () => {
|
||||||
if (!currentBehaviorTree.value?.graph) {
|
nextTick(() => {
|
||||||
currentBehaviorTree.value?.graph = {
|
graph.value?.clearCells();
|
||||||
nodes: [],
|
if (currentBehaviorTree.value?.graph && graph.value) {
|
||||||
edges: [],
|
if (currentBehaviorTree.value?.graph.nodes) {
|
||||||
};
|
currentBehaviorTree.value?.graph.nodes.forEach(ele => {
|
||||||
}
|
const node = createModelNode(ele as TaskNodeElement);
|
||||||
if(graph.value && currentNodeGraph.value){
|
console.info('create node: ', ele);
|
||||||
graph.value.clearCells();
|
// 将节点添加到画布
|
||||||
setTimeout(()=> {
|
|
||||||
nextTick(()=> {
|
|
||||||
try{
|
|
||||||
const nodes: EdgeNodeElement[] = currentBehaviorTree.value?.graph?.nodes ?? [];
|
|
||||||
const edges: EdgeNodeElement[] = currentBehaviorTree.value?.graph?.edges ?? [];
|
|
||||||
nodes.forEach((n: any) => {
|
|
||||||
const node = createTaskNodeElement(n);
|
|
||||||
graph.value?.addNode(node as Node);
|
graph.value?.addNode(node as Node);
|
||||||
})
|
});
|
||||||
edges.forEach((g: any) => {
|
|
||||||
graph.value?.addEdge( g as any);
|
|
||||||
})
|
|
||||||
} catch (e){
|
|
||||||
console.warn('createElements',e)
|
|
||||||
}
|
}
|
||||||
})
|
if (currentBehaviorTree.value?.graph.edges) {
|
||||||
}, 200)
|
// 然后添加所有边,确保包含桩点信息
|
||||||
|
setTimeout(() => {
|
||||||
|
currentBehaviorTree.value?.graph.edges.forEach(edgeData => {
|
||||||
|
graph.value?.addEdge({
|
||||||
|
...edgeData,
|
||||||
|
...createLineOptions(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, 100); // 延迟一会儿,免得连线错位
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 初始化X6画布
|
// 初始化X6画布
|
||||||
|
|||||||
@@ -10,4 +10,3 @@
|
|||||||
export * from './tree'
|
export * from './tree'
|
||||||
export * from './template'
|
export * from './template'
|
||||||
export * from './parameter'
|
export * from './parameter'
|
||||||
export * from './node'
|
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
* that was distributed with this source code.
|
* that was distributed with this source code.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ApiDataResponse, ApiPaginationQuery, NullableString } from '@/types';
|
import type { ApiDataResponse, NullableString } from '@/types';
|
||||||
|
|
||||||
export interface NodeTemplate {
|
export interface NodeTemplate {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -19,16 +19,6 @@ export interface NodeTemplate {
|
|||||||
englishName: NullableString;
|
englishName: NullableString;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NodeTemplateData {
|
|
||||||
templates: NodeTemplate[];
|
|
||||||
total: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NodeTemplateQuery extends ApiPaginationQuery {
|
|
||||||
include_params: boolean;
|
|
||||||
type: NullableString;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NodeTemplatesResponse extends ApiDataResponse<NodeTemplate[]> {
|
export interface NodeTemplatesResponse extends ApiDataResponse<NodeTemplate[]> {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ApiDataResponse, NullableString, PageableResponse } from '@/types';
|
import type { ApiDataResponse, NullableString, PageableResponse } from '@/types';
|
||||||
import type { NodeGraph } from './node';
|
import type { NodeGraph } from '../builder/element';
|
||||||
|
|
||||||
export interface BehaviorTree {
|
export interface BehaviorTree {
|
||||||
id: number,
|
id: number,
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
* that was distributed with this source code.
|
* that was distributed with this source code.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Edge, Graph, Node } from '@antv/x6';
|
import type { NodeTemplate } from '../types';
|
||||||
import type { EdgeNodeElement, NodeGraph, NodeTemplate, SettingTaskNodeElement, TaskNodeElement, TaskNodeRect } from '../types';
|
import type { SettingTaskNodeElement, TaskNodeRect } from '../builder/element';
|
||||||
import { generateKey } from '@/utils/strings.ts';
|
import { generateKey } from '@/utils/strings';
|
||||||
|
|
||||||
export const createTaskNodeElementFromTemplate = (
|
export const createTaskNodeElementFromTemplate = (
|
||||||
template: NodeTemplate,
|
template: NodeTemplate,
|
||||||
@@ -31,7 +31,7 @@ export const createTaskNodeElementFromTemplate = (
|
|||||||
},
|
},
|
||||||
width: realRect.width,
|
width: realRect.width,
|
||||||
height: realRect.height,
|
height: realRect.height,
|
||||||
settings: JSON.parse(JSON.stringify(template.parameter_defs ?? [])),
|
// settings: JSON.parse(JSON.stringify(template.parameter_defs ?? [])),
|
||||||
inputs: null,
|
inputs: null,
|
||||||
outputs: null,
|
outputs: null,
|
||||||
parameters: {},
|
parameters: {},
|
||||||
@@ -53,94 +53,3 @@ export const createTaskNodeElementFromTemplate = (
|
|||||||
],
|
],
|
||||||
} as SettingTaskNodeElement;
|
} as SettingTaskNodeElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createTaskNodeElement = (element: TaskNodeElement, width: number = 200, height: number = 100): any => {
|
|
||||||
return {
|
|
||||||
shape: 'task',
|
|
||||||
id: element.key,
|
|
||||||
position: {
|
|
||||||
x: element.position?.x || 0,
|
|
||||||
y: element.position?.y || 0,
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
width: element.width ?? width,
|
|
||||||
height: element.height ?? height,
|
|
||||||
},
|
|
||||||
attrs: {
|
|
||||||
label: {
|
|
||||||
text: element.name,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data: element,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const resolveNodeTaskElements = (graph: Graph): TaskNodeElement[] => {
|
|
||||||
const taskElements: TaskNodeElement[] = [];
|
|
||||||
if (graph) {
|
|
||||||
const nodes = graph?.getNodes() as Node[];
|
|
||||||
if (nodes) {
|
|
||||||
nodes.forEach(node => {
|
|
||||||
const nodeData = node.getData() as TaskNodeElement;
|
|
||||||
const newElement = {
|
|
||||||
...nodeData,
|
|
||||||
key: node.id,
|
|
||||||
position: node.getPosition(),
|
|
||||||
width: node.getSize().width,
|
|
||||||
height: node.getSize().height,
|
|
||||||
};
|
|
||||||
taskElements.push(newElement);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return taskElements;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const resolveNodeEdgeElements = (graph: Graph): EdgeNodeElement[] => {
|
|
||||||
const edgeElements: EdgeNodeElement[] = [];
|
|
||||||
if (graph) {
|
|
||||||
const graphEdges = graph?.getEdges() ?? [] as Edge[];
|
|
||||||
if (graphEdges) {
|
|
||||||
graphEdges.forEach(edge => {
|
|
||||||
const nodeData = edge.getData() as TaskNodeElement;
|
|
||||||
edgeElements.push({
|
|
||||||
id: nodeData?.id ?? 0,
|
|
||||||
key: edge.id,
|
|
||||||
type: 'edge',
|
|
||||||
status: nodeData?.status,
|
|
||||||
source: edge.getSource() ? edge.getSource() as unknown as string : null,
|
|
||||||
target: edge.getSource() ? edge.getTarget() as unknown as string : null,
|
|
||||||
attrs: edge.getAttrs() ?? {},
|
|
||||||
router: edge.getRouter() ?? {},
|
|
||||||
connector: edge.getConnector() ?? null,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return edgeElements;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const resolveNodeGraph = (graph: Graph): NodeGraph => {
|
|
||||||
const nodes: TaskNodeElement[] = resolveNodeTaskElements(graph);
|
|
||||||
const edges: EdgeNodeElement[] = resolveNodeEdgeElements(graph);
|
|
||||||
return {
|
|
||||||
nodes,
|
|
||||||
edges,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const hasElements = (graph: Graph): boolean => {
|
|
||||||
if (graph) {
|
|
||||||
const taskElements: TaskNodeElement[] = resolveNodeTaskElements(graph);
|
|
||||||
return taskElements.length > 0;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const hasRootElementNode = (graph: Graph): boolean => {
|
|
||||||
if (graph) {
|
|
||||||
const taskElements: TaskNodeElement[] = resolveNodeTaskElements(graph);
|
|
||||||
return taskElements.filter(e => e.type === 'root').length === 1;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user