Commit 7417de74 by heheer Committed by GitHub

feat: add workflow helperline (#2356)

* feat: add workflow helperline

* del console

* across zoom & adjust distance
parent fe9cb437
...@@ -80,3 +80,17 @@ export type SystemPluginTemplateItemType = WorkflowTemplateType & { ...@@ -80,3 +80,17 @@ export type SystemPluginTemplateItemType = WorkflowTemplateType & {
workflow: WorkflowTemplateBasicType; workflow: WorkflowTemplateBasicType;
}; };
export type THelperLine = {
position: number;
nodes: {
left: number;
right: number;
top: number;
bottom: number;
width: number;
height: number;
centerX: number;
centerY: number;
}[];
};
import { THelperLine } from '@fastgpt/global/core/workflow/type';
import { CSSProperties, useEffect, useRef } from 'react';
import { ReactFlowState, useStore, useViewport } from 'reactflow';
const canvasStyle: CSSProperties = {
width: '100%',
height: '100%',
position: 'absolute',
zIndex: 10,
pointerEvents: 'none'
};
const storeSelector = (state: ReactFlowState) => ({
width: state.width,
height: state.height,
transform: state.transform
});
export type HelperLinesProps = {
horizontal?: THelperLine;
vertical?: THelperLine;
};
function HelperLinesRenderer({ horizontal, vertical }: HelperLinesProps) {
const { width, height, transform } = useStore(storeSelector);
const { zoom } = useViewport();
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
const ctx = canvas?.getContext('2d');
if (!ctx || !canvas) {
return;
}
const dpi = window.devicePixelRatio;
canvas.width = width * dpi;
canvas.height = height * dpi;
ctx.scale(dpi, dpi);
ctx.clearRect(0, 0, width, height);
ctx.strokeStyle = '#D92D20';
const drawCross = (x: number, y: number, size: number) => {
ctx.beginPath();
ctx.moveTo(x - size, y - size);
ctx.lineTo(x + size, y + size);
ctx.moveTo(x + size, y - size);
ctx.lineTo(x - size, y + size);
ctx.stroke();
};
if (vertical) {
const x = vertical.position * transform[2] + transform[0];
ctx.beginPath();
ctx.moveTo(
x,
Math.min(...vertical.nodes.map((node) => node.top)) * transform[2] + transform[1]
);
ctx.lineTo(
x,
Math.max(...vertical.nodes.map((node) => node.bottom)) * transform[2] + transform[1]
);
ctx.stroke();
vertical.nodes.forEach((node) => {
drawCross(x, node.top * transform[2] + transform[1], 5 * zoom);
drawCross(x, node.bottom * transform[2] + transform[1], 5 * zoom);
});
}
if (horizontal) {
const y = horizontal.position * transform[2] + transform[1];
ctx.beginPath();
ctx.moveTo(
Math.min(...horizontal.nodes.map((node) => node.left)) * transform[2] + transform[0],
y
);
ctx.lineTo(
Math.max(...horizontal.nodes.map((node) => node.right)) * transform[2] + transform[0],
y
);
ctx.stroke();
horizontal.nodes.forEach((node) => {
drawCross(node.left * transform[2] + transform[0], y, 5 * zoom);
drawCross(node.right * transform[2] + transform[0], y, 5 * zoom);
});
}
}, [width, height, transform, horizontal, vertical]);
return <canvas ref={canvasRef} style={canvasStyle} />;
}
export default HelperLinesRenderer;
import React, { useCallback, useMemo } from 'react'; import React, { useCallback, useMemo } from 'react';
import { Connection, NodeChange, OnConnectStartParams, addEdge, EdgeChange, Edge } from 'reactflow'; import {
Connection,
NodeChange,
OnConnectStartParams,
addEdge,
EdgeChange,
Edge,
applyNodeChanges,
Node
} from 'reactflow';
import { EDGE_TYPE } from '@fastgpt/global/core/workflow/node/constant'; import { EDGE_TYPE } from '@fastgpt/global/core/workflow/node/constant';
import 'reactflow/dist/style.css'; import 'reactflow/dist/style.css';
import { useToast } from '@fastgpt/web/hooks/useToast'; import { useToast } from '@fastgpt/web/hooks/useToast';
...@@ -8,6 +17,7 @@ import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; ...@@ -8,6 +17,7 @@ import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useKeyboard } from './useKeyboard'; import { useKeyboard } from './useKeyboard';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context'; import { WorkflowContext } from '../../context';
import { useWorkflowUtils } from './useUtils';
export const useWorkflow = () => { export const useWorkflow = () => {
const { toast } = useToast(); const { toast } = useToast();
...@@ -18,12 +28,47 @@ export const useWorkflow = () => { ...@@ -18,12 +28,47 @@ export const useWorkflow = () => {
}); });
const { isDowningCtrl } = useKeyboard(); const { isDowningCtrl } = useKeyboard();
const { setConnectingEdge, nodes, onNodesChange, setEdges, onEdgesChange, setHoverEdgeId } = const {
useContextSelector(WorkflowContext, (v) => v); setConnectingEdge,
nodes,
setNodes,
onNodesChange,
setEdges,
onEdgesChange,
setHoverEdgeId,
setHelperLineHorizontal,
setHelperLineVertical
} = useContextSelector(WorkflowContext, (v) => v);
const { getHelperLines } = useWorkflowUtils();
const customApplyNodeChanges = useCallback((changes: NodeChange[], nodes: Node[]): Node[] => {
setHelperLineHorizontal(undefined);
setHelperLineVertical(undefined);
if (
changes.length === 1 &&
changes[0].type === 'position' &&
changes[0].dragging &&
changes[0].position
) {
const helperLines = getHelperLines(changes[0], nodes);
changes[0].position.x = helperLines.snapPosition.x ?? changes[0].position.x;
changes[0].position.y = helperLines.snapPosition.y ?? changes[0].position.y;
setHelperLineHorizontal(helperLines.horizontal);
setHelperLineVertical(helperLines.vertical);
}
return applyNodeChanges(changes, nodes);
}, []);
/* node */ /* node */
const handleNodesChange = useCallback( const handleNodesChange = useCallback(
(changes: NodeChange[]) => { (changes: NodeChange[]) => {
setNodes((nodes) => customApplyNodeChanges(changes, nodes));
for (const change of changes) { for (const change of changes) {
if (change.type === 'remove') { if (change.type === 'remove') {
const node = nodes.find((n) => n.id === change.id); const node = nodes.find((n) => n.id === change.id);
......
...@@ -26,6 +26,7 @@ import { useContextSelector } from 'use-context-selector'; ...@@ -26,6 +26,7 @@ import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../context'; import { WorkflowContext } from '../context';
import { useWorkflow } from './hooks/useWorkflow'; import { useWorkflow } from './hooks/useWorkflow';
import { t } from 'i18next'; import { t } from 'i18next';
import HelperLines from './components/HelperLines';
const NodeSimple = dynamic(() => import('./nodes/NodeSimple')); const NodeSimple = dynamic(() => import('./nodes/NodeSimple'));
const nodeTypes: Record<FlowNodeTypeEnum, any> = { const nodeTypes: Record<FlowNodeTypeEnum, any> = {
...@@ -62,7 +63,8 @@ const edgeTypes = { ...@@ -62,7 +63,8 @@ const edgeTypes = {
}; };
const Workflow = () => { const Workflow = () => {
const { nodes, edges, reactFlowWrapper } = useContextSelector(WorkflowContext, (v) => v); const { nodes, edges, reactFlowWrapper, helperLineHorizontal, helperLineVertical } =
useContextSelector(WorkflowContext, (v) => v);
const { const {
ConfirmDeleteModal, ConfirmDeleteModal,
...@@ -135,6 +137,7 @@ const Workflow = () => { ...@@ -135,6 +137,7 @@ const Workflow = () => {
onEdgeMouseLeave={onEdgeMouseLeave} onEdgeMouseLeave={onEdgeMouseLeave}
> >
<FlowController /> <FlowController />
<HelperLines horizontal={helperLineHorizontal} vertical={helperLineVertical} />
</ReactFlow> </ReactFlow>
</Box> </Box>
......
...@@ -48,6 +48,7 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; ...@@ -48,6 +48,7 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { formatTime2HM, formatTime2YMDHMW } from '@fastgpt/global/common/string/time'; import { formatTime2HM, formatTime2YMDHMW } from '@fastgpt/global/common/string/time';
import type { InitProps } from '@/pages/app/detail/components/PublishHistoriesSlider'; import type { InitProps } from '@/pages/app/detail/components/PublishHistoriesSlider';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import { THelperLine } from '@fastgpt/global/core/workflow/type';
type OnChange<ChangesType> = (changes: ChangesType[]) => void; type OnChange<ChangesType> = (changes: ChangesType[]) => void;
...@@ -135,6 +136,12 @@ type WorkflowContextType = { ...@@ -135,6 +136,12 @@ type WorkflowContextType = {
historiesDefaultData?: InitProps; historiesDefaultData?: InitProps;
setHistoriesDefaultData: React.Dispatch<React.SetStateAction<undefined | InitProps>>; setHistoriesDefaultData: React.Dispatch<React.SetStateAction<undefined | InitProps>>;
// helper line
helperLineHorizontal?: THelperLine;
setHelperLineHorizontal: React.Dispatch<React.SetStateAction<THelperLine | undefined>>;
helperLineVertical?: THelperLine;
setHelperLineVertical: React.Dispatch<React.SetStateAction<THelperLine | undefined>>;
// chat test // chat test
setWorkflowTestData: React.Dispatch< setWorkflowTestData: React.Dispatch<
React.SetStateAction< React.SetStateAction<
...@@ -260,6 +267,14 @@ export const WorkflowContext = createContext<WorkflowContextType>({ ...@@ -260,6 +267,14 @@ export const WorkflowContext = createContext<WorkflowContextType>({
setHistoriesDefaultData: function (value: React.SetStateAction<InitProps | undefined>): void { setHistoriesDefaultData: function (value: React.SetStateAction<InitProps | undefined>): void {
throw new Error('Function not implemented.'); throw new Error('Function not implemented.');
}, },
helperLineHorizontal: undefined,
setHelperLineHorizontal: function (value: React.SetStateAction<THelperLine | undefined>): void {
throw new Error('Function not implemented.');
},
helperLineVertical: undefined,
setHelperLineVertical: function (value: React.SetStateAction<THelperLine | undefined>): void {
throw new Error('Function not implemented.');
},
getNodeDynamicInputs: function (nodeId: string): FlowNodeInputItemType[] { getNodeDynamicInputs: function (nodeId: string): FlowNodeInputItemType[] {
throw new Error('Function not implemented.'); throw new Error('Function not implemented.');
} }
...@@ -728,6 +743,11 @@ const WorkflowContextProvider = ({ ...@@ -728,6 +743,11 @@ const WorkflowContextProvider = ({
/* Version histories */ /* Version histories */
const [historiesDefaultData, setHistoriesDefaultData] = useState<InitProps>(); const [historiesDefaultData, setHistoriesDefaultData] = useState<InitProps>();
/* helper line */
const [helperLineHorizontal, setHelperLineHorizontal] = useState<THelperLine | undefined>(
undefined
);
const [helperLineVertical, setHelperLineVertical] = useState<THelperLine | undefined>(undefined);
/* event bus */ /* event bus */
useEffect(() => { useEffect(() => {
eventBus.on(EventNameEnum.requestWorkflowStore, () => { eventBus.on(EventNameEnum.requestWorkflowStore, () => {
...@@ -797,6 +817,12 @@ const WorkflowContextProvider = ({ ...@@ -797,6 +817,12 @@ const WorkflowContextProvider = ({
historiesDefaultData, historiesDefaultData,
setHistoriesDefaultData, setHistoriesDefaultData,
// helper line
helperLineHorizontal,
setHelperLineHorizontal,
helperLineVertical,
setHelperLineVertical,
// chat test // chat test
setWorkflowTestData setWorkflowTestData
}; };
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment