feat: Make edges work

This commit is contained in:
Braks
2021-07-09 13:39:17 +02:00
parent 52e1188d04
commit b2c250ef38
10 changed files with 183 additions and 99 deletions
+3 -3
View File
@@ -1,15 +1,15 @@
<template> <template>
<RevueFlow style="height: 1000px" :elements="elements"> Let's flow! </RevueFlow> <Basic style="height: 100vh" />
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import RevueFlow from './container/RevueFlow'; import Basic from './Basic';
export default defineComponent({ export default defineComponent({
name: 'App', name: 'App',
components: { components: {
RevueFlow Basic
}, },
data() { data() {
return { return {
+85
View File
@@ -0,0 +1,85 @@
import { Connection, Edge, Elements, FlowElement, Node, OnLoadParams } from './types';
import { addEdge, isNode, removeElements } from './utils/graph';
import RevueFlow from './container/RevueFlow';
import { defineComponent, ref } from 'vue';
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element);
const initialElements: Elements = [
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } },
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } },
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } },
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } },
{ id: 'e1-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3' }
];
const BasicFlow = defineComponent({
setup() {
const elements = ref<Elements>(initialElements);
const rfInstance = ref<OnLoadParams | null>(null);
const onElementsRemove = (elementsToRemove: Elements) => (elements.value = removeElements(elementsToRemove, elements.value));
const onConnect = (params: Edge | Connection) => (elements.value = addEdge(params, elements.value));
const onLoad = (reactFlowInstance: OnLoadParams) => {
console.log('loaded', reactFlowInstance);
rfInstance.value = reactFlowInstance;
};
const updatePos = () => {
elements.value = elements.value.map((el) => {
if (isNode(el)) {
el.position = {
x: Math.random() * 400,
y: Math.random() * 400
};
}
return el;
});
};
const logToObject = () => console.log(rfInstance.value?.toObject());
const resetTransform = () => rfInstance.value?.setTransform({ x: 0, y: 0, zoom: 1 });
const toggleClassnames = () => {
elements.value = elements.value.map((el) => {
if (isNode(el)) {
el.className = el.className === 'light' ? 'dark' : 'light';
}
return el;
});
};
return () => (
<RevueFlow
elements={elements.value}
onLoad={onLoad}
onElementClick={onElementClick}
onElementsRemove={onElementsRemove}
onConnect={onConnect}
onNodeDragStop={onNodeDragStop}
class="react-flow-basic-example"
defaultZoom={1.5}
minZoom={0.2}
maxZoom={4}
>
<div style={{ position: 'absolute', right: '10px', top: '10px', zIndex: 4 }}>
<button onClick={resetTransform} style={{ marginRight: '5px' }}>
reset transform
</button>
<button onClick={updatePos} style={{ marginRight: '5px' }}>
change pos
</button>
<button onClick={toggleClassnames} style={{ marginRight: '5px' }}>
toggle classnames
</button>
<button onClick={logToObject}>toObject</button>
</div>
</RevueFlow>
);
}
});
export default BasicFlow;
+26 -20
View File
@@ -1,4 +1,4 @@
import { CSSProperties, defineComponent, PropType } from 'vue'; import { computed, CSSProperties, defineComponent, PropType } from 'vue';
import EdgeText from './EdgeText'; import EdgeText from './EdgeText';
@@ -96,9 +96,9 @@ const BezierEdge = defineComponent({
required: true required: true
}, },
labelBgPadding: { labelBgPadding: {
type: ([0, 0] as any) as PropType<[number, number]>, type: Array as unknown as PropType<[number, number]>,
required: false, required: false,
default: () => [0, 0] as [number, number] default: () => [0, 0]
}, },
labelBgBorderRadius: { labelBgBorderRadius: {
type: Number, type: Number,
@@ -120,26 +120,32 @@ const BezierEdge = defineComponent({
} }
}, },
setup(props) { setup(props) {
const [centerX, centerY] = getCenter({ console.log('edge');
sourceX: props.sourceX, const centered = computed(() => {
sourceY: props.sourceY, return getCenter({
targetX: props.targetX, sourceX: props.sourceX,
targetY: props.targetY, sourceY: props.sourceY,
sourcePosition: props.sourcePosition, targetX: props.targetX,
targetPosition: props.targetPosition targetY: props.targetY,
sourcePosition: props.sourcePosition,
targetPosition: props.targetPosition
});
}); });
const path = getBezierPath({ const path = computed(() => {
sourceX: props.sourceX, console.log('computing in edge');
sourceY: props.sourceY, return getBezierPath({
targetX: props.targetX, sourceX: props.sourceX,
targetY: props.targetY, sourceY: props.sourceY,
targetPosition: props.targetPosition targetX: props.targetX,
targetY: props.targetY,
targetPosition: props.targetPosition
});
}); });
const text = props.label ? ( const text = props.label ? (
<EdgeText <EdgeText
x={centerX} x={centered.value[0]}
y={centerY} y={centered.value[1]}
label={props.label} label={props.label}
labelStyle={props.labelStyle} labelStyle={props.labelStyle}
labelShowBg={props.labelShowBg} labelShowBg={props.labelShowBg}
@@ -151,9 +157,9 @@ const BezierEdge = defineComponent({
const markerEnd = getMarkerEnd(props.arrowHeadType, props.markerEndId); const markerEnd = getMarkerEnd(props.arrowHeadType, props.markerEndId);
return ( return () => (
<> <>
<path style={props.style} d={path} class="react-flow__edge-path" marker-end={markerEnd} /> <path style={props.style} d={path.value} class="react-flow__edge-path" marker-end={markerEnd} />
{text} {text}
</> </>
); );
+1 -4
View File
@@ -10,16 +10,13 @@ export default (EdgeComponent: any): Component<EdgeProps> => {
components: { EdgeComponent }, components: { EdgeComponent },
props: WrapEdgeProps, props: WrapEdgeProps,
setup(props) { setup(props) {
console.log('wrap');
const pinia = store(); const pinia = store();
const updating = ref<boolean>(false); const updating = ref<boolean>(false);
const inactive = !props.elementsSelectable && !props.onClick; const inactive = !props.elementsSelectable && !props.onClick;
const edgeClasses = [ const edgeClasses = [
'react-flow__edge', 'react-flow__edge',
`react-flow__edge-${props.type}`, `react-flow__edge-${props.type}`,
{ selected: props.selected, animated: props.animated, inactive, updating } { selected: props.selected, animated: props.animated, inactive, updating: updating.value }
]; ];
const edgeElement = computed<Edge>(() => { const edgeElement = computed<Edge>(() => {
+43 -53
View File
@@ -14,7 +14,7 @@ import {
Transform, Transform,
OnEdgeUpdateFunc OnEdgeUpdateFunc
} from '../../types'; } from '../../types';
import { computed, CSSProperties, defineComponent, PropType, ref } from 'vue'; import { computed, CSSProperties, defineComponent, PropType } from 'vue';
import store from '../../store'; import store from '../../store';
interface EdgeRendererProps { interface EdgeRendererProps {
@@ -102,71 +102,59 @@ const EdgeCmp = defineComponent({
} }
}, },
setup(props) { setup(props) {
const sourceHandleId = props.edge.sourceHandle || null; const sourceHandleId = computed(() => props.edge.sourceHandle ?? null);
const targetHandleId = props.edge.targetHandle || null; const targetHandleId = computed(() => props.edge.targetHandle ?? null);
const nodes = computed(() => { const nodes = computed(() => getSourceTargetNodes(props.edge, props.nodes));
console.log('foo');
return getSourceTargetNodes(props.edge, props.nodes);
});
const { sourceNode, targetNode } = nodes.value;
console.log(nodes.value);
const onConnectEdge = (connection: Connection) => { const onConnectEdge = (connection: Connection) => {
props.props.onEdgeUpdate?.(props.edge, connection); props.props.onEdgeUpdate?.(props.edge, connection);
}; };
if (!sourceNode) { if (!nodes.value.sourceNode) {
console.log(`couldn't create edge for source id: ${props.edge.source}; edge id: ${props.edge.id}`); console.log(`couldn't create edge for source id: ${props.edge.source}; edge id: ${props.edge.id}`);
return null; return () => null;
} }
if (!targetNode) { if (!nodes.value.targetNode) {
console.log(`couldn't create edge for target id: ${props.edge.target}; edge id: ${props.edge.id}`); console.log(`couldn't create edge for target id: ${props.edge.target}; edge id: ${props.edge.id}`);
return null; return () => null;
}
// source and target node need to be initialized
if (!sourceNode.__rf.width || !targetNode.__rf.width) {
return null;
} }
const edgeType = props.edge.type || 'default'; const edgeType = props.edge.type || 'default';
const EdgeComponent: any = props.props.edgeTypes[edgeType] || props.props.edgeTypes.default; const EdgeComponent: any = props.props.edgeTypes[edgeType] || props.props.edgeTypes.default;
const targetNodeBounds = targetNode.__rf.handleBounds; const targetNodeBounds = nodes.value.targetNode.__rf.handleBounds;
// when connection type is loose we can define all handles as sources // when connection type is loose we can define all handles as sources
const targetNodeHandles = const targetNodeHandles =
props.connectionMode === ConnectionMode.Strict props.connectionMode === ConnectionMode.Strict
? targetNodeBounds.target ? targetNodeBounds.target
: targetNodeBounds.target || targetNodeBounds.source; : targetNodeBounds.target || targetNodeBounds.source;
const sourceHandle = getHandle(sourceNode.__rf.handleBounds.source, sourceHandleId); const sourceHandle = computed(
const targetHandle = getHandle(targetNodeHandles, targetHandleId); () => nodes.value.sourceNode && getHandle(nodes.value.sourceNode.__rf.handleBounds.source, sourceHandleId.value)
const sourcePosition = sourceHandle ? sourceHandle.position : Position.Bottom;
const targetPosition = targetHandle ? targetHandle.position : Position.Top;
if (!sourceHandle) {
console.log(`couldn't create edge for source handle id: ${sourceHandleId}; edge id: ${props.edge.id}`);
return null;
}
if (!targetHandle) {
console.log(`couldn't create edge for target handle id: ${targetHandleId}; edge id: ${props.edge.id}`);
return null;
}
const { sourceX, sourceY, targetX, targetY } = getEdgePositions(
sourceNode,
sourceHandle,
sourcePosition,
targetNode,
targetHandle,
targetPosition
); );
const targetHandle = computed(() => getHandle(targetNodeHandles, targetHandleId.value));
const sourcePosition = sourceHandle.value ? sourceHandle.value.position : Position.Bottom;
const targetPosition = targetHandle.value ? targetHandle.value.position : Position.Top;
const edgePositions = computed(() => {
return (
nodes.value.sourceNode &&
nodes.value.targetNode &&
getEdgePositions(
nodes.value.sourceNode,
sourceHandle,
sourcePosition,
nodes.value.targetNode,
targetHandle,
targetPosition
)
);
});
const isVisible = computed(() => { const isVisible = computed(() => {
return props.onlyRenderVisibleElements return props.onlyRenderVisibleElements
? isEdgeVisible({ ? isEdgeVisible({
sourcePos: { x: sourceX, y: sourceY }, sourcePos: { x: edgePositions.value?.sourceX, y: edgePositions.value?.sourceY },
targetPos: { x: targetX, y: targetY }, targetPos: { x: edgePositions.value?.targetX, y: edgePositions.value?.targetY },
width: props.width || 0, width: props.width || 0,
height: props.height || 0, height: props.height || 0,
transform: props.transform transform: props.transform
@@ -202,10 +190,10 @@ const EdgeCmp = defineComponent({
target={props.edge.target} target={props.edge.target}
sourceHandleId={sourceHandleId} sourceHandleId={sourceHandleId}
targetHandleId={targetHandleId} targetHandleId={targetHandleId}
sourceX={sourceX} sourceX={edgePositions.value.sourceX}
sourceY={sourceY} sourceY={edgePositions.value.sourceY}
targetX={targetX} targetX={edgePositions.value.targetX}
targetY={targetY} targetY={edgePositions.value.targetY}
sourcePosition={sourcePosition} sourcePosition={sourcePosition}
targetPosition={targetPosition} targetPosition={targetPosition}
elementsSelectable={props.elementsSelectable} elementsSelectable={props.elementsSelectable}
@@ -228,7 +216,10 @@ const EdgeCmp = defineComponent({
const EdgeRenderer = defineComponent({ const EdgeRenderer = defineComponent({
name: 'EdgeRenderer', name: 'EdgeRenderer',
components: { EdgeCmp, ConnectionLine }, components: {
EdgeCmp,
ConnectionLine
},
props: { props: {
edgeTypes: { edgeTypes: {
type: Object, type: Object,
@@ -323,13 +314,12 @@ const EdgeRenderer = defineComponent({
}, },
setup(props) { setup(props) {
const pinia = store(); const pinia = store();
const transformStyle = computed(() => `translate(${pinia.transform[0]},${pinia.transform[1]}) scale(${pinia.transform[2]})`);
const transformStyle = `translate(${pinia.transform[0]},${pinia.transform[1]}) scale(${pinia.transform[2]})`; const renderConnectionLine = computed(() => pinia.connectionNodeId && pinia.connectionHandleType);
const renderConnectionLine = pinia.connectionNodeId && pinia.connectionHandleType;
return () => ( return () => (
<svg width={pinia.width} height={pinia.height} class="react-flow__edges"> <svg width={pinia.width} height={pinia.height} class="react-flow__edges">
<g transform={transformStyle}> <g transform={transformStyle.value}>
{pinia.edges.map((edge: Edge) => ( {pinia.edges.map((edge: Edge) => (
<EdgeCmp <EdgeCmp
key={edge.id} key={edge.id}
@@ -344,7 +334,7 @@ const EdgeRenderer = defineComponent({
onlyRenderVisibleElements={props.onlyRenderVisibleElements} onlyRenderVisibleElements={props.onlyRenderVisibleElements}
/> />
))} ))}
{renderConnectionLine && ( {renderConnectionLine.value && (
<ConnectionLine <ConnectionLine
nodes={pinia.nodes} nodes={pinia.nodes}
connectionNodeId={pinia.connectionNodeId!} connectionNodeId={pinia.connectionNodeId!}
+1 -1
View File
@@ -59,7 +59,6 @@ export function getHandlePosition(position: Position, node: Node, handle: any |
} }
export function getHandle(bounds: HandleElement[], handleId: ElementId | null): HandleElement | null { export function getHandle(bounds: HandleElement[], handleId: ElementId | null): HandleElement | null {
console.log('handle', bounds);
if (!bounds) { if (!bounds) {
return null; return null;
} }
@@ -94,6 +93,7 @@ export const getEdgePositions = (
const sourceHandlePos = getHandlePosition(sourcePosition, sourceNode, sourceHandle); const sourceHandlePos = getHandlePosition(sourcePosition, sourceNode, sourceHandle);
const targetHandlePos = getHandlePosition(targetPosition, targetNode, targetHandle); const targetHandlePos = getHandlePosition(targetPosition, targetNode, targetHandle);
console.log('getting edge positions');
return { return {
sourceX: sourceHandlePos.x, sourceX: sourceHandlePos.x,
sourceY: sourceHandlePos.y, sourceY: sourceHandlePos.y,
+15 -5
View File
@@ -6,7 +6,18 @@ import { onLoadProject, onLoadGetElements, onLoadToObject } from '../../utils/gr
import { ReactFlowProps } from '../RevueFlow'; import { ReactFlowProps } from '../RevueFlow';
import { NodeTypesType, EdgeTypesType, ConnectionLineType, KeyCode } from '../../types'; import { NodeTypesType, EdgeTypesType, ConnectionLineType, KeyCode } from '../../types';
import { CSSProperties, defineComponent, onMounted, PropType, ref } from 'vue'; import {
reactive,
computed,
CSSProperties,
defineComponent,
onBeforeMount,
onMounted,
onUpdated,
PropType,
ref,
watch
} from 'vue';
import store from '../../store'; import store from '../../store';
import useZoomPanHelper from '../../hooks/useZoomPanHelper'; import useZoomPanHelper from '../../hooks/useZoomPanHelper';
@@ -350,9 +361,9 @@ const GraphView = defineComponent({
const pinia = store(); const pinia = store();
const isInitialized = ref<boolean>(false); const isInitialized = ref<boolean>(false);
const { zoomIn, zoomOut, zoomTo, transform, fitView, initialized } = useZoomPanHelper();
onMounted(() => { onMounted(() => {
const { zoomIn, zoomOut, zoomTo, transform, fitView, initialized } = useZoomPanHelper();
if (!isInitialized.value && initialized) { if (!isInitialized.value && initialized) {
if (props.onLoad) { if (props.onLoad) {
props.onLoad({ props.onLoad({
@@ -364,9 +375,8 @@ const GraphView = defineComponent({
project: onLoadProject(pinia), project: onLoadProject(pinia),
getElements: onLoadGetElements(pinia), getElements: onLoadGetElements(pinia),
toObject: onLoadToObject(pinia) toObject: onLoadToObject(pinia)
} as any); });
} }
isInitialized.value = true; isInitialized.value = true;
} }
}); });
+5 -8
View File
@@ -1,4 +1,4 @@
import { computed, CSSProperties, defineComponent, HTMLAttributes, onMounted, PropType, provide } from 'vue'; import { computed, CSSProperties, defineComponent, HTMLAttributes, onBeforeMount, PropType, provide } from 'vue';
import GraphView from '../GraphView'; import GraphView from '../GraphView';
import DefaultNode from '../../components/Nodes/DefaultNode'; import DefaultNode from '../../components/Nodes/DefaultNode';
import InputNode from '../../components/Nodes/InputNode'; import InputNode from '../../components/Nodes/InputNode';
@@ -454,14 +454,11 @@ const RevueFlow = defineComponent({
}, },
setup(props, { slots }) { setup(props, { slots }) {
const pinia = store(); const pinia = store();
const nodeTypesParsed = computed(() => createNodeTypes(props.nodeTypes as any)); pinia.setElements(props.elements);
const edgeTypesParsed = computed(() => createEdgeTypes(props.edgeTypes as any));
provide('store', pinia);
const reactFlowClasses = ['react-flow'];
onMounted(() => { const nodeTypesParsed = computed(() => props.nodeTypes && createNodeTypes(props.nodeTypes));
pinia.setElements(props.elements); const edgeTypesParsed = computed(() => props.edgeTypes && createEdgeTypes(props.edgeTypes));
}); const reactFlowClasses = ['react-flow'];
return () => ( return () => (
<div class={reactFlowClasses}> <div class={reactFlowClasses}>
+1 -1
View File
@@ -14,7 +14,7 @@ export default (rendererNode: HTMLDivElement) => {
const size = getDimensions(rendererNode); const size = getDimensions(rendererNode);
if (size.height === 0 || size.width === 0) { if (size.height === 0 || size.width === 0) {
console.warn('The React Flow parent container needs a width and a height to render the graph.'); console.log('The React Flow parent container needs a width and a height to render the graph.');
} }
pinia.updateSize(size); pinia.updateSize(size);
+3 -4
View File
@@ -1,7 +1,6 @@
import { zoomIdentity } from 'd3-zoom'; import { zoomIdentity } from 'd3-zoom';
import { getRectOfNodes, pointToRendererPoint, getTransformForBounds } from '../utils/graph'; import { getRectOfNodes, pointToRendererPoint, getTransformForBounds } from '../utils/graph';
import { FitViewParams, FlowTransform, ZoomPanHelperFunctions, Rect, XYPosition } from '../types'; import { FitViewParams, FlowTransform, ZoomPanHelperFunctions, Rect, XYPosition } from '../types';
import { computed } from 'vue';
import store from '../store'; import store from '../store';
const DEFAULT_PADDING = 0.1; const DEFAULT_PADDING = 0.1;
@@ -20,7 +19,7 @@ const initialZoomPanHelper: ZoomPanHelperFunctions = {
const useZoomPanHelper = (): ZoomPanHelperFunctions => { const useZoomPanHelper = (): ZoomPanHelperFunctions => {
const pinia = store(); const pinia = store();
const zoomPanHelperFunctions = computed<ZoomPanHelperFunctions>(() => { const zoomPanHelperFunctions = () => {
if (pinia.d3Selection && pinia.d3Zoom) { if (pinia.d3Selection && pinia.d3Zoom) {
return { return {
zoomIn: () => pinia.d3Zoom?.scaleBy(pinia.d3Selection as any, 1.2), zoomIn: () => pinia.d3Zoom?.scaleBy(pinia.d3Selection as any, 1.2),
@@ -77,9 +76,9 @@ const useZoomPanHelper = (): ZoomPanHelperFunctions => {
} }
return initialZoomPanHelper; return initialZoomPanHelper;
}); };
return zoomPanHelperFunctions.value; return zoomPanHelperFunctions();
}; };
export default useZoomPanHelper; export default useZoomPanHelper;