From 0bfbf475f1d9b642a2a2f2d49392d5a5ca5769da Mon Sep 17 00:00:00 2001 From: Michael Charfadi Date: Tue, 14 Nov 2023 15:32:44 +0100 Subject: [PATCH] [2572] Use dynamic handles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug: https://github.com/eclipse-sirius/sirius-web/issues/2572 Signed-off-by: Michaƫl Charfadi --- CHANGELOG.adoc | 2 +- .../src/converter/ConvertEngine.types.ts | 4 +- .../IconLabelNodeConverterHandler.ts | 6 +- .../converter/ImageNodeConverterHandler.ts | 16 ++- .../src/converter/ListNodeConverterHandler.ts | 16 ++- .../RectangleNodeConverterHandler.ts | 16 ++- .../src/converter/convertDiagram.ts | 21 +++- .../src/converter/convertHandles.ts | 64 +++++++++++ .../src/index.ts | 7 +- .../src/renderer/DiagramRenderer.tsx | 3 + .../src/renderer/DiagramRenderer.types.ts | 2 + .../src/renderer/edge/EdgeLayout.ts | 106 ++++++++++++----- .../src/renderer/edge/EdgeLayout.types.ts | 46 ++++++-- .../src/renderer/edge/MultiLabelEdge.tsx | 35 +++--- .../renderer/handles/ConnectionHandles.tsx | 108 ++++++++++++++++++ .../handles/ConnectionHandles.types.ts | 23 ++++ .../src/renderer/handles/useHandleChange.tsx | 90 +++++++++++++++ .../renderer/handles/useHandleChange.types.ts | 17 +++ .../handles/useRefreshConnectionHandles.tsx | 41 +++++++ .../useRefreshConnectionHandles.types.ts | 18 +++ .../src/renderer/layout/layout.tsx | 11 +- .../src/renderer/layout/layoutHandles.ts | 66 +++++++++++ .../src/renderer/node/ImageNode.tsx | 43 ++----- .../src/renderer/node/ListNode.tsx | 42 ++----- .../src/renderer/node/RectangularNode.tsx | 42 ++----- .../sirius-web/src/nodes/EllipseNode.tsx | 41 ++----- .../src/nodes/EllipseNodeConverterHandler.ts | 16 ++- 27 files changed, 680 insertions(+), 222 deletions(-) create mode 100644 packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/converter/convertHandles.ts create mode 100644 packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/handles/ConnectionHandles.tsx create mode 100644 packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/handles/ConnectionHandles.types.ts create mode 100644 packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/handles/useHandleChange.tsx create mode 100644 packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/handles/useHandleChange.types.ts create mode 100644 packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/handles/useRefreshConnectionHandles.tsx create mode 100644 packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/handles/useRefreshConnectionHandles.types.ts create mode 100644 packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/layoutHandles.ts diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 9d66e70d24..e0be03b2fd 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -133,7 +133,7 @@ The new implementation of `IEditService`, named `ComposedEditService`, tries fir - https://github.com/eclipse-sirius/sirius-web/issues/2555[#2555] [diagram] Add distinct edge creation handles (only visible when a node is selected). - https://github.com/eclipse-sirius/sirius-web/issues/2559[#2559] [diagram] Allow to target the whole node when creating or reconnecting an edge. - https://github.com/eclipse-sirius/sirius-web/issues/2235[#2235] [diagram] Reduce the amount of code in DiagramRenderer - +- https://github.com/eclipse-sirius/sirius-web/issues/2572[#2572] [diagram] Each edges now have a dedicated handle. == v2023.10.0 diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/converter/ConvertEngine.types.ts b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/converter/ConvertEngine.types.ts index b129cddbd5..c76c55ee78 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/converter/ConvertEngine.types.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/converter/ConvertEngine.types.ts @@ -12,6 +12,7 @@ *******************************************************************************/ import { Node } from 'reactflow'; import { GQLNodeDescription } from '../graphql/query/nodeDescriptionFragment.types'; +import { GQLEdge } from '../graphql/subscription/edgeFragment.types'; import { GQLNode, GQLNodeStyle } from '../graphql/subscription/nodeFragment.types'; export interface IConvertEngine { @@ -32,6 +33,7 @@ export interface INodeConverterHandler { parentNode: GQLNode | null, isBorderNode: boolean, nodes: Node[], - nodeDescriptions: GQLNodeDescription[] + nodeDescriptions: GQLNodeDescription[], + gqlEdges: GQLEdge[] ): void; } diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/converter/IconLabelNodeConverterHandler.ts b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/converter/IconLabelNodeConverterHandler.ts index 93a761d1af..1e4b4ca77d 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/converter/IconLabelNodeConverterHandler.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/converter/IconLabelNodeConverterHandler.ts @@ -19,9 +19,10 @@ import { GQLViewModifier, } from '../graphql/subscription/nodeFragment.types'; import { BorderNodePositon } from '../renderer/DiagramRenderer.types'; +import { ConnectionHandle } from '../renderer/handles/ConnectionHandles.types'; import { IconLabelNodeData } from '../renderer/node/IconsLabelNode.types'; -import { convertLabelStyle } from './convertDiagram'; import { IConvertEngine, INodeConverterHandler } from './ConvertEngine.types'; +import { convertLabelStyle } from './convertDiagram'; const defaultPosition: XYPosition = { x: 0, y: 0 }; @@ -43,6 +44,8 @@ const toIconLabelNode = ( labelEditable, } = gqlNode; + const connectionHandles: ConnectionHandle[] = []; + const data: IconLabelNodeData = { targetObjectId, targetObjectLabel, @@ -60,6 +63,7 @@ const toIconLabelNode = ( defaultWidth: gqlNode.defaultWidth, defaultHeight: gqlNode.defaultHeight, labelEditable: labelEditable, + connectionHandles, }; if (insideLabel) { diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/converter/ImageNodeConverterHandler.ts b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/converter/ImageNodeConverterHandler.ts index 8fe2de51e2..07c6a18638 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/converter/ImageNodeConverterHandler.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/converter/ImageNodeConverterHandler.ts @@ -12,12 +12,15 @@ *******************************************************************************/ import { Node, XYPosition } from 'reactflow'; import { GQLNodeDescription } from '../graphql/query/nodeDescriptionFragment.types'; +import { GQLEdge } from '../graphql/subscription/edgeFragment.types'; import { GQLImageNodeStyle, GQLNode, GQLNodeStyle, GQLViewModifier } from '../graphql/subscription/nodeFragment.types'; import { BorderNodePositon } from '../renderer/DiagramRenderer.types'; +import { ConnectionHandle } from '../renderer/handles/ConnectionHandles.types'; import { ImageNodeData } from '../renderer/node/ImageNode.types'; +import { IConvertEngine, INodeConverterHandler } from './ConvertEngine.types'; import { convertLabelStyle } from './convertDiagram'; import { AlignmentMap } from './convertDiagram.types'; -import { IConvertEngine, INodeConverterHandler } from './ConvertEngine.types'; +import { convertHandles } from './convertHandles'; const defaultPosition: XYPosition = { x: 0, y: 0 }; @@ -25,7 +28,8 @@ const toImageNode = ( gqlNode: GQLNode, gqlParentNode: GQLNode | null, nodeDescription: GQLNodeDescription | undefined, - isBorderNode: boolean + isBorderNode: boolean, + gqlEdges: GQLEdge[] ): Node => { const { targetObjectId, @@ -39,6 +43,8 @@ const toImageNode = ( labelEditable, } = gqlNode; + const connectionHandles: ConnectionHandle[] = convertHandles(gqlNode, gqlEdges); + const data: ImageNodeData = { targetObjectId, targetObjectLabel, @@ -55,6 +61,7 @@ const toImageNode = ( borderNodePosition: isBorderNode ? BorderNodePositon.WEST : null, labelEditable, positionDependentRotation: style.positionDependentRotation, + connectionHandles, }; if (insideLabel) { @@ -112,10 +119,11 @@ export class ImageNodeConverterHandler implements INodeConverterHandler { parentNode: GQLNode | null, isBorderNode: boolean, nodes: Node[], - nodeDescriptions: GQLNodeDescription[] + nodeDescriptions: GQLNodeDescription[], + gqlEdges: GQLEdge[] ) { const nodeDescription = nodeDescriptions.find((description) => description.id === gqlNode.descriptionId); - nodes.push(toImageNode(gqlNode, parentNode, nodeDescription, isBorderNode)); + nodes.push(toImageNode(gqlNode, parentNode, nodeDescription, isBorderNode, gqlEdges)); convertEngine.convertNodes(gqlNode.borderNodes ?? [], gqlNode, nodes, nodeDescriptions); convertEngine.convertNodes(gqlNode.childNodes ?? [], gqlNode, nodes, nodeDescriptions); } diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/converter/ListNodeConverterHandler.ts b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/converter/ListNodeConverterHandler.ts index e4240b495c..477bd64638 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/converter/ListNodeConverterHandler.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/converter/ListNodeConverterHandler.ts @@ -12,6 +12,7 @@ *******************************************************************************/ import { Node, XYPosition } from 'reactflow'; import { GQLNodeDescription } from '../graphql/query/nodeDescriptionFragment.types'; +import { GQLEdge } from '../graphql/subscription/edgeFragment.types'; import { GQLNode, GQLNodeStyle, @@ -19,10 +20,12 @@ import { GQLViewModifier, } from '../graphql/subscription/nodeFragment.types'; import { BorderNodePositon } from '../renderer/DiagramRenderer.types'; +import { ConnectionHandle } from '../renderer/handles/ConnectionHandles.types'; import { ListNodeData } from '../renderer/node/ListNode.types'; +import { IConvertEngine, INodeConverterHandler } from './ConvertEngine.types'; import { convertLabelStyle } from './convertDiagram'; import { AlignmentMap } from './convertDiagram.types'; -import { IConvertEngine, INodeConverterHandler } from './ConvertEngine.types'; +import { convertHandles } from './convertHandles'; const defaultPosition: XYPosition = { x: 0, y: 0 }; @@ -30,7 +33,8 @@ const toListNode = ( gqlNode: GQLNode, gqlParentNode: GQLNode | null, nodeDescription: GQLNodeDescription | undefined, - isBorderNode: boolean + isBorderNode: boolean, + gqlEdges: GQLEdge[] ): Node => { const { targetObjectId, @@ -44,6 +48,8 @@ const toListNode = ( labelEditable, } = gqlNode; + const connectionHandles: ConnectionHandle[] = convertHandles(gqlNode, gqlEdges); + const data: ListNodeData = { targetObjectId, targetObjectLabel, @@ -62,6 +68,7 @@ const toListNode = ( faded: state === GQLViewModifier.Faded, labelEditable, nodeDescription, + connectionHandles, defaultWidth: gqlNode.defaultWidth, defaultHeight: gqlNode.defaultHeight, }; @@ -126,10 +133,11 @@ export class ListNodeConverterHandler implements INodeConverterHandler { parentNode: GQLNode | null, isBorderNode: boolean, nodes: Node[], - nodeDescriptions: GQLNodeDescription[] + nodeDescriptions: GQLNodeDescription[], + gqlEdges: GQLEdge[] ) { const nodeDescription = nodeDescriptions.find((description) => description.id === gqlNode.descriptionId); - nodes.push(toListNode(gqlNode, parentNode, nodeDescription, isBorderNode)); + nodes.push(toListNode(gqlNode, parentNode, nodeDescription, isBorderNode, gqlEdges)); convertEngine.convertNodes(gqlNode.borderNodes ?? [], gqlNode, nodes, nodeDescriptions); convertEngine.convertNodes(gqlNode.childNodes ?? [], gqlNode, nodes, nodeDescriptions); } diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/converter/RectangleNodeConverterHandler.ts b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/converter/RectangleNodeConverterHandler.ts index a90f85b699..dc19cbb609 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/converter/RectangleNodeConverterHandler.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/converter/RectangleNodeConverterHandler.ts @@ -12,6 +12,7 @@ *******************************************************************************/ import { Node, XYPosition } from 'reactflow'; import { GQLNodeDescription } from '../graphql/query/nodeDescriptionFragment.types'; +import { GQLEdge } from '../graphql/subscription/edgeFragment.types'; import { GQLNode, GQLNodeStyle, @@ -19,10 +20,12 @@ import { GQLViewModifier, } from '../graphql/subscription/nodeFragment.types'; import { BorderNodePositon } from '../renderer/DiagramRenderer.types'; +import { ConnectionHandle } from '../renderer/handles/ConnectionHandles.types'; import { RectangularNodeData } from '../renderer/node/RectangularNode.types'; +import { IConvertEngine, INodeConverterHandler } from './ConvertEngine.types'; import { convertLabelStyle } from './convertDiagram'; import { AlignmentMap } from './convertDiagram.types'; -import { IConvertEngine, INodeConverterHandler } from './ConvertEngine.types'; +import { convertHandles } from './convertHandles'; const defaultPosition: XYPosition = { x: 0, y: 0 }; @@ -30,7 +33,8 @@ const toRectangularNode = ( gqlNode: GQLNode, gqlParentNode: GQLNode | null, nodeDescription: GQLNodeDescription | undefined, - isBorderNode: boolean + isBorderNode: boolean, + gqlEdges: GQLEdge[] ): Node => { const { targetObjectId, @@ -44,6 +48,8 @@ const toRectangularNode = ( labelEditable, } = gqlNode; + const connectionHandles: ConnectionHandle[] = convertHandles(gqlNode, gqlEdges); + const data: RectangularNodeData = { targetObjectId, targetObjectLabel, @@ -65,6 +71,7 @@ const toRectangularNode = ( isBorderNode: isBorderNode, borderNodePosition: isBorderNode ? BorderNodePositon.EAST : null, labelEditable, + connectionHandles, }; if (insideLabel) { @@ -127,10 +134,11 @@ export class RectangleNodeConverterHandler implements INodeConverterHandler { parentNode: GQLNode | null, isBorderNode: boolean, nodes: Node[], - nodeDescriptions: GQLNodeDescription[] + nodeDescriptions: GQLNodeDescription[], + gqlEdges: GQLEdge[] ) { const nodeDescription = nodeDescriptions.find((description) => description.id === gqlNode.descriptionId); - nodes.push(toRectangularNode(gqlNode, parentNode, nodeDescription, isBorderNode)); + nodes.push(toRectangularNode(gqlNode, parentNode, nodeDescription, isBorderNode, gqlEdges)); convertEngine.convertNodes(gqlNode.borderNodes ?? [], gqlNode, nodes, nodeDescriptions); convertEngine.convertNodes(gqlNode.childNodes ?? [], gqlNode, nodes, nodeDescriptions); } diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/converter/convertDiagram.ts b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/converter/convertDiagram.ts index 469b00d7d2..07fb37e5d9 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/converter/convertDiagram.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/converter/convertDiagram.ts @@ -112,7 +112,7 @@ export const convertDiagram = ( if (nodeConverterHandler) { const isBorderNode: boolean = !!parentNode?.borderNodes?.map((borderNode) => borderNode.id).includes(node.id); - nodeConverterHandler.handle(this, node, parentNode, isBorderNode, nodes, nodeDescriptions); + nodeConverterHandler.handle(this, node, parentNode, isBorderNode, nodes, nodeDescriptions, gqlDiagram.edges); } }); }, @@ -125,8 +125,10 @@ export const convertDiagram = ( const nodeId2Depth = new Map(); nodes.forEach((node) => nodeId2Depth.set(node.id, nodeDepth(nodeId2node, node.id))); - + let usedHandles: string[] = []; const edges: Edge[] = gqlDiagram.edges.map((gqlEdge) => { + const sourceNode: Node | undefined = nodeId2node.get(gqlEdge.sourceId); + const targetNode: Node | undefined = nodeId2node.get(gqlEdge.targetId); const data: MultiLabelEdgeData = { targetObjectId: gqlEdge.targetObjectId, targetObjectKind: gqlEdge.targetObjectKind, @@ -145,6 +147,17 @@ export const convertDiagram = ( data.endLabel = convertEdgeLabel(gqlEdge.endLabel); } + const sourceHandle = sourceNode?.data.connectionHandles + .filter((handle) => handle.type === 'source') + .find((handle) => !usedHandles.find((usedHandles) => usedHandles === handle.id)); + + const targetHandle = targetNode?.data.connectionHandles + .filter((handle) => handle.type === 'target') + .find((handle) => !usedHandles.find((usedHandles) => usedHandles === handle.id)); + if (sourceHandle?.id && targetHandle?.id) { + usedHandles.push(sourceHandle?.id, targetHandle.id); + } + return { id: gqlEdge.id, type: 'multiLabelEdge', @@ -159,6 +172,10 @@ export const convertDiagram = ( }, data, hidden: gqlEdge.state === GQLViewModifier.Hidden, + sourceHandle: sourceHandle?.id, + targetHandle: targetHandle?.id, + sourceNode: sourceNode, + targetNode: targetNode, }; }); diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/converter/convertHandles.ts b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/converter/convertHandles.ts new file mode 100644 index 0000000000..6abb368dbc --- /dev/null +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/converter/convertHandles.ts @@ -0,0 +1,64 @@ +/******************************************************************************* + * Copyright (c) 2023 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +import { Position } from 'reactflow'; +import { GQLEdge } from '../graphql/subscription/edgeFragment.types'; +import { GQLNode, GQLNodeStyle } from '../graphql/subscription/nodeFragment.types'; +import { ConnectionHandle } from '../renderer/handles/ConnectionHandles.types'; + +const numberSourceHandles = (handles: ConnectionHandle[]): string => { + return handles.filter((handle) => handle.type === 'source').length.toString(); +}; +const numberTargetHandles = (handles: ConnectionHandle[]): string => { + return handles.filter((handle) => handle.type === 'target').length.toString(); +}; +export const convertHandles = (gqlNode: GQLNode, gqlEdges: GQLEdge[]): ConnectionHandle[] => { + const connectionHandles: ConnectionHandle[] = []; + gqlEdges.forEach((edge) => { + if (edge.sourceId === gqlNode.id) { + connectionHandles.push({ + id: `handle--source--${gqlNode.id}--${numberSourceHandles(connectionHandles)}`, + edgeId: edge.id, + nodeId: gqlNode.id, + position: Position.Right, + type: 'source', + }); + } + + if (edge.targetId === gqlNode.id) { + connectionHandles.push({ + id: `handle--target--${gqlNode.id}--${numberTargetHandles(connectionHandles)}`, + edgeId: edge.id, + nodeId: gqlNode.id, + position: Position.Left, + type: 'target', + }); + } + }); + connectionHandles.push({ + id: `handle--source--${gqlNode.id}--${numberSourceHandles(connectionHandles)}`, + edgeId: '', + nodeId: gqlNode.id, + position: Position.Right, + type: 'source', + }); + + connectionHandles.push({ + id: `handle--target--${gqlNode.id}--${numberTargetHandles(connectionHandles)}`, + edgeId: '', + nodeId: gqlNode.id, + position: Position.Left, + type: 'target', + }); + + return connectionHandles; +}; diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/index.ts b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/index.ts index 239a6fe5c0..5e3934e770 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/index.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/index.ts @@ -16,9 +16,11 @@ export type { NodeTypeContextValue } from './contexts/NodeContext.types'; export type { IConvertEngine, INodeConverterHandler } from './converter/ConvertEngine.types'; export { convertLabelStyle } from './converter/convertDiagram'; export { AlignmentMap } from './converter/convertDiagram.types'; +export { convertHandles } from './converter/convertHandles'; +export type { GQLNodeDescription } from './graphql/query/nodeDescriptionFragment.types'; +export type { GQLEdge } from './graphql/subscription/edgeFragment.types'; export { GQLViewModifier } from './graphql/subscription/nodeFragment.types'; export type { GQLNode, GQLNodeStyle, GraphQLNodeStyleFragment } from './graphql/subscription/nodeFragment.types'; -export type { GQLNodeDescription } from './graphql/query/nodeDescriptionFragment.types'; export { BorderNodePositon } from './renderer/DiagramRenderer.types'; export type { Diagram, NodeData } from './renderer/DiagramRenderer.types'; export { Label } from './renderer/Label'; @@ -26,7 +28,10 @@ export { useConnector } from './renderer/connector/useConnector'; export { useDrop } from './renderer/drop/useDrop'; export { useDropNode } from './renderer/dropNode/useDropNode'; export { ConnectionCreationHandles } from './renderer/handles/ConnectionCreationHandles'; +export { ConnectionHandles } from './renderer/handles/ConnectionHandles'; +export type { ConnectionHandle } from './renderer/handles/ConnectionHandles.types'; export { ConnectionTargetHandle } from './renderer/handles/ConnectionTargetHandle'; +export { useRefreshConnectionHandles } from './renderer/handles/useRefreshConnectionHandles'; export type { ILayoutEngine, INodeLayoutHandler } from './renderer/layout/LayoutEngine.types'; export * from './renderer/layout/layoutBorderNodes'; export * from './renderer/layout/layoutNode'; diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/DiagramRenderer.tsx b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/DiagramRenderer.tsx index b04fd15106..ac1772bcbf 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/DiagramRenderer.tsx +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/DiagramRenderer.tsx @@ -41,6 +41,7 @@ import { useDropNode } from './dropNode/useDropNode'; import { edgeTypes } from './edge/EdgeTypes'; import { MultiLabelEdgeData } from './edge/MultiLabelEdge.types'; import { useInitialFitToScreen } from './fit-to-screen/useInitialFitToScreen'; +import { useHandleChange } from './handles/useHandleChange'; import { useLayout } from './layout/useLayout'; import { DiagramNodeType } from './node/NodeTypes.types'; import { useNodeType } from './node/useNodeType'; @@ -74,6 +75,7 @@ export const DiagramRenderer = ({ const { reconnectEdge } = useReconnectEdge(); const { onDrop, onDragOver } = useDrop(); const { onBorderChange } = useBorderChange(); + const { onHandleChange } = useHandleChange(); const { getNodeTypes } = useNodeType(); const [nodes, setNodes, onNodesChange] = useNodesState([]); @@ -108,6 +110,7 @@ export const DiagramRenderer = ({ const handleNodesChange: OnNodesChange = (changes: NodeChange[]) => { onNodesChange(onBorderChange(changes)); + onNodesChange(onHandleChange(changes)); updateSelectionOnNodesChange(changes); }; diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/DiagramRenderer.types.ts b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/DiagramRenderer.types.ts index f22125631b..66d623b399 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/DiagramRenderer.types.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/DiagramRenderer.types.ts @@ -16,6 +16,7 @@ import { Edge, Node } from 'reactflow'; import { GQLDiagramDescription, GQLNodeDescription } from '../graphql/query/nodeDescriptionFragment.types'; import { GQLDiagramRefreshedEventPayload } from '../graphql/subscription/diagramEventSubscription.types'; import { MultiLabelEdgeData } from './edge/MultiLabelEdge.types'; +import { ConnectionHandle } from './handles/ConnectionHandles.types'; import { DiagramNodeType } from './node/NodeTypes.types'; export interface DiagramRendererProps { @@ -58,6 +59,7 @@ export interface NodeData { borderNodePosition: BorderNodePositon | null; labelEditable: boolean; style: React.CSSProperties; + connectionHandles: ConnectionHandle[]; } export enum BorderNodePositon { diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/edge/EdgeLayout.ts b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/edge/EdgeLayout.ts index b5c7eeb3df..dd238f8b8d 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/edge/EdgeLayout.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/edge/EdgeLayout.ts @@ -12,26 +12,60 @@ *******************************************************************************/ import { HandleElement, Position, internalsSymbol } from 'reactflow'; -import { GetEdgeParameters, GetHandleCoordinatesByPosition, GetNodeCenter, GetParameters } from './EdgeLayout.types'; +import { + GetEdgeParameters, + GetEdgeParametersWhileMoving, + GetHandleCoordinatesByPosition, + GetNodeCenter, + GetParameters, + NodeCenter, +} from './EdgeLayout.types'; -export const getEdgeParameters: GetEdgeParameters = (source, target) => { - const { x: sourceX, y: sourceY, position: sourcePosition } = getParameters(source, target); - const { x: targetX, y: targetY, position: targetPosition } = getParameters(target, source); +export const getEdgeParametersWhileMoving: GetEdgeParametersWhileMoving = ( + movingNode, + source, + target, + visiblesNodes +) => { + const { position: sourcePosition } = getParameters(movingNode, source, target, visiblesNodes); + const { position: targetPosition } = getParameters(movingNode, target, source, visiblesNodes); return { - sourceX, - sourceY, sourcePosition, - targetX, - targetY, targetPosition, }; }; -const getParameters: GetParameters = (nodeA, nodeB) => { - const centerA = getNodeCenter(nodeA); - const centerB = getNodeCenter(nodeB); +export const getEdgeParameters: GetEdgeParameters = (source, target, visiblesNodes) => { + const { position: sourcePosition } = getParameters(null, source, target, visiblesNodes); + const { position: targetPosition } = getParameters(null, target, source, visiblesNodes); + return { + sourcePosition, + targetPosition, + }; +}; + +const getParameters: GetParameters = (movingNode, nodeA, nodeB, visiblesNodes) => { + let centerA: NodeCenter; + if (movingNode && movingNode.id === nodeA.id) { + centerA = { + x: movingNode.positionAbsolute?.x ?? 0 + (nodeA.width ?? 0) / 2, + y: movingNode.positionAbsolute?.y ?? 0 + (nodeA.height ?? 0) / 2, + }; + } else { + centerA = getNodeCenter(nodeA, visiblesNodes); + } + + let centerB: NodeCenter; + if (movingNode && movingNode.id === nodeB.id) { + centerB = { + x: movingNode.positionAbsolute?.x ?? 0 + (nodeB.width ?? 0) / 2, + y: movingNode.positionAbsolute?.y ?? 0 + (nodeB.height ?? 0) / 2, + }; + } else { + centerB = getNodeCenter(nodeB, visiblesNodes); + } const horizontallDifference = Math.abs(centerA.x - centerB.x); const verticalDifference = Math.abs(centerA.y - centerB.y); @@ -42,43 +76,59 @@ const getParameters: GetParameters = (nodeA, nodeB) => { position = centerA.y > centerB.y ? Position.Top : Position.Bottom; } - const { x, y } = getHandleCoordinatesByPosition(nodeA, position); - return { - x, - y, position, }; }; -const getNodeCenter: GetNodeCenter = (node) => { - return { - x: node.positionAbsolute?.x ?? 0 + (node.width ?? 0) / 2, - y: node.positionAbsolute?.y ?? 0 + (node.height ?? 0) / 2, - }; +const getNodeCenter: GetNodeCenter = (node, visiblesNodes) => { + if (node.positionAbsolute?.x && node.positionAbsolute?.y) { + return { + x: node.positionAbsolute?.x ?? 0 + (node.width ?? 0) / 2, + y: node.positionAbsolute?.y ?? 0 + (node.height ?? 0) / 2, + }; + } else { + let parentNode = visiblesNodes.find((nodeParent) => nodeParent.id === node.parentNode); + let position = { + x: node.position?.x ?? 0 + (node.width ?? 0) / 2, + y: node.position?.y ?? 0 + (node.height ?? 0) / 2, + }; + while (parentNode) { + position = { + x: position.x + parentNode.position?.x ?? 0, + y: position.y + parentNode.position?.y ?? 0, + }; + let parentNodeId = parentNode.parentNode ?? ''; + parentNode = visiblesNodes.find((nodeParent) => nodeParent.id === parentNodeId); + } + return position; + } }; -const getHandleCoordinatesByPosition: GetHandleCoordinatesByPosition = (node, handlePosition) => { - const handle: HandleElement | undefined = (node[internalsSymbol]?.handleBounds?.source ?? []).find( - (handle) => handle.position === handlePosition +export const getHandleCoordinatesByPosition: GetHandleCoordinatesByPosition = (node, handlePosition, handleId) => { + let handle: HandleElement | undefined = (node[internalsSymbol]?.handleBounds?.source ?? []).find( + (handle) => handle.id === handleId ); + if (!handle) { + handle = (node[internalsSymbol]?.handleBounds?.target ?? []).find((handle) => handle.id === handleId); + } - if (handle) { + if (handle && handlePosition) { let offsetX = handle.width / 2; let offsetY = handle.height / 2; switch (handlePosition) { case Position.Left: - offsetX = 0; + offsetX = handle.width; break; case Position.Right: - offsetX = handle.width; + offsetX = 0; break; case Position.Top: - offsetY = 0; + offsetY = handle.height; break; case Position.Bottom: - offsetY = handle.height; + offsetY = 0; break; } diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/edge/EdgeLayout.types.ts b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/edge/EdgeLayout.types.ts index 8df0c35304..58b27f75f0 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/edge/EdgeLayout.types.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/edge/EdgeLayout.types.ts @@ -11,36 +11,60 @@ * Obeo - initial API and implementation *******************************************************************************/ -import { Node, Position } from 'reactflow'; +import { Node, NodePositionChange, Position, XYPosition } from 'reactflow'; import { NodeData } from '../DiagramRenderer.types'; +export interface EdgeParameters { + sourcePosition: Position; + targetPosition: Position; +} -export type GetEdgeParameters = (source: Node, target: Node) => EdgeParameters; +export type GetEdgeParametersWhileMoving = ( + movingNode: NodePositionChange, + source: Node, + target: Node, + visiblesNodes: Node[] +) => EdgeParameters; + +export type GetEdgeParameters = ( + source: Node, + target: Node, + visiblesNodes: Node[] +) => EdgeParameters; export interface EdgeParameters { - sourceX: number; - sourceY: number; sourcePosition: Position; - targetX: number; - targetY: number; targetPosition: Position; } -export type GetParameters = (nodeA: Node, nodeB: Node) => Parameters; +export type GetParameters = ( + movingNode: NodePositionChange | null, + nodeA: Node, + nodeB: Node, + visiblesNodes: Node[] +) => Parameters; export interface Parameters { - x: number; - y: number; position: Position; } -export type GetNodeCenter = (node: Node) => NodeCenter; +export type GetNodeCenter = (node: Node, visiblesNodes: Node[]) => NodeCenter; export interface NodeCenter { x: number; y: number; } -export type GetHandleCoordinatesByPosition = (node: Node, handlePosition: Position) => HandleCoordinates; +export type GetHandleCoordinatesByPosition = ( + node: Node, + handlePosition: Position, + handleId: string +) => XYPosition; + +export type GetHandleCoordinatesByPosition2 = ( + node: Node, + handlePosition: Position, + edgeId: string +) => HandleCoordinates; export interface HandleCoordinates { x: number; diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/edge/MultiLabelEdge.tsx b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/edge/MultiLabelEdge.tsx index d97e19b74b..dd0ed7d967 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/edge/MultiLabelEdge.tsx +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/edge/MultiLabelEdge.tsx @@ -27,7 +27,7 @@ import { import { EdgeData, NodeData } from '../DiagramRenderer.types'; import { Label } from '../Label'; import { DiagramElementPalette } from '../palette/DiagramElementPalette'; -import { getEdgeParameters } from './EdgeLayout'; +import { getHandleCoordinatesByPosition } from './EdgeLayout'; import { MultiLabelEdgeData } from './MultiLabelEdge.types'; const multiLabelEdgeStyle = ( @@ -59,10 +59,13 @@ export const MultiLabelEdge = memo( markerEnd, markerStart, selected, + sourcePosition, + targetPosition, sourceHandleId, targetHandleId, }: EdgeProps) => { const theme = useTheme(); + const reactFlowInstance = useReactFlow(); const sourceNode = useStore | undefined>( useCallback((store: ReactFlowState) => store.nodeInternals.get(source), [source]) @@ -75,10 +78,8 @@ export const MultiLabelEdge = memo( return null; } - const { sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition } = getEdgeParameters( - sourceNode, - targetNode - ); + const { x: sourceX, y: sourceY } = getHandleCoordinatesByPosition(sourceNode, sourcePosition, sourceHandleId ?? ''); + const { x: targetX, y: targetY } = getHandleCoordinatesByPosition(targetNode, targetPosition, targetHandleId ?? ''); const [edgePath, labelX, labelY] = getSmoothStepPath({ sourceX, @@ -91,20 +92,20 @@ export const MultiLabelEdge = memo( const { beginLabel, endLabel, label, faded } = data || {}; - const reactFlowInstance = useReactFlow(); useEffect(() => { - if (sourceHandleId?.split('--')[2] !== sourcePosition || targetHandleId?.split('--')[2] !== targetPosition) { - reactFlowInstance.setEdges((edges: Edge[]) => - edges.map((edge) => { - if (edge.id === id) { - edge.sourceHandle = `handle--${edge.source}--${sourcePosition}`; - edge.targetHandle = `handle--${edge.target}--${targetPosition}`; + reactFlowInstance.setEdges((edges: Edge[]) => + edges.map((edge) => { + if (edge.id === id) { + if (selected) { + edge.updatable = true; + } else { + edge.updatable = false; } - return edge; - }) - ); - } - }, [sourcePosition, targetPosition]); + } + return edge; + }) + ); + }, [selected]); return ( <> diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/handles/ConnectionHandles.tsx b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/handles/ConnectionHandles.tsx new file mode 100644 index 0000000000..d8840f42d7 --- /dev/null +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/handles/ConnectionHandles.tsx @@ -0,0 +1,108 @@ +/******************************************************************************* + * Copyright (c) 2023 Obeo and others. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +import { Theme, useTheme } from '@material-ui/core/styles'; +import React from 'react'; +import { Handle, Position, useReactFlow } from 'reactflow'; +import { EdgeData, NodeData } from '../DiagramRenderer.types'; +import { ConnectionHandlesProps } from './ConnectionHandles.types'; + +const borderHandlesStyle = (position: Position): React.CSSProperties => { + const style: React.CSSProperties = { + display: 'flex', + position: 'absolute', + justifyContent: 'space-evenly', + }; + switch (position) { + case Position.Left: + style.height = '100%'; + style.left = '0'; + style.top = '0'; + style.flexDirection = 'column'; + break; + case Position.Right: + style.height = '100%'; + style.right = '0'; + style.top = '0'; + style.flexDirection = 'column'; + break; + case Position.Top: + style.width = '100%'; + style.left = '0'; + style.top = '0'; + style.flexDirection = 'row'; + break; + case Position.Bottom: + style.width = '100%'; + style.left = '0'; + style.bottom = '0'; + style.flexDirection = 'row'; + break; + } + return style; +}; + +const handleStyle = (theme: Theme, position: Position, isEdgeSelected: boolean): React.CSSProperties => { + const style: React.CSSProperties = { + position: 'relative', + transform: 'none', + opacity: '0', + pointerEvents: 'none', + }; + switch (position) { + case Position.Left: + case Position.Right: + style.top = 'auto'; + break; + case Position.Top: + case Position.Bottom: + style.left = 'auto'; + break; + } + if (isEdgeSelected) { + style.opacity = 1; + style.outline = `${theme.palette.primary.main} solid 1px`; + } + return style; +}; + +export const ConnectionHandles = ({ connectionHandles }: ConnectionHandlesProps) => { + const theme = useTheme(); + const reactFlowInstance = useReactFlow(); + const isEdgeSelected = (edgeId: string) => { + return !!reactFlowInstance.getEdge(edgeId)?.selected; + }; + return ( + <> + {Object.values(Position).map((key) => { + return ( +
+ {connectionHandles + .filter((customHandle) => customHandle.position === key) + .map((customHandle) => { + return ( + + ); + })} +
+ ); + })} + + ); +}; diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/handles/ConnectionHandles.types.ts b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/handles/ConnectionHandles.types.ts new file mode 100644 index 0000000000..dba9a890ba --- /dev/null +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/handles/ConnectionHandles.types.ts @@ -0,0 +1,23 @@ +/******************************************************************************* + * Copyright (c) 2023 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ + +import { HandleProps } from 'reactflow'; + +export interface ConnectionHandlesProps { + connectionHandles: ConnectionHandle[]; +} + +export interface ConnectionHandle extends HandleProps { + edgeId: string; + nodeId: string; +} diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/handles/useHandleChange.tsx b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/handles/useHandleChange.tsx new file mode 100644 index 0000000000..78fab8422f --- /dev/null +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/handles/useHandleChange.tsx @@ -0,0 +1,90 @@ +/******************************************************************************* + * Copyright (c) 2023 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +import { Node, NodeChange, getConnectedEdges, useReactFlow } from 'reactflow'; +import { EdgeData, NodeData } from '../DiagramRenderer.types'; +import { getEdgeParametersWhileMoving } from '../edge/EdgeLayout'; +import { ConnectionHandle } from './ConnectionHandles.types'; +import { UseHandleChangeValue } from './useHandleChange.types'; +export const useHandleChange = (): UseHandleChangeValue => { + const { getEdges, getNodes, setNodes } = useReactFlow(); + + const onHandleChange = (changes: NodeChange[]): NodeChange[] => { + return changes.map((change) => { + if (change.type === 'position' && change.positionAbsolute) { + const movedNode = getNodes().find((node) => change.id === node.id); + + if (movedNode) { + const connectedEdges = getConnectedEdges([movedNode], getEdges()); + connectedEdges.forEach((edge) => { + const { sourceHandle, targetHandle, id } = edge; + const sourceNode = getNodes().find((node) => node.id === edge.sourceNode?.id); + const targetNode = getNodes().find((node) => node.id === edge.targetNode?.id); + + if (sourceNode && targetNode) { + const { sourcePosition, targetPosition } = getEdgeParametersWhileMoving( + change, + sourceNode, + targetNode, + getNodes() + ); + const nodeSourceConnectionHandle: ConnectionHandle | undefined = sourceNode.data.connectionHandles.find( + (connectionHandle: ConnectionHandle) => connectionHandle.id === sourceHandle + ); + const nodeTargetConnectionHandle: ConnectionHandle | undefined = targetNode.data.connectionHandles.find( + (connectionHandle: ConnectionHandle) => connectionHandle.id === targetHandle + ); + + if ( + nodeSourceConnectionHandle?.position !== sourcePosition && + nodeTargetConnectionHandle?.position !== targetPosition + ) { + const nodeSourceConnectionHandles: ConnectionHandle[] = sourceNode.data.connectionHandles.map( + (nodeConnectionHandle: ConnectionHandle) => { + if (nodeConnectionHandle.edgeId === id && nodeConnectionHandle.type === 'source') { + nodeConnectionHandle.position = sourcePosition; + } + return nodeConnectionHandle; + } + ); + + const nodeTargetConnectionHandles: ConnectionHandle[] = targetNode.data.connectionHandles.map( + (nodeConnectionHandle: ConnectionHandle) => { + if (nodeConnectionHandle.edgeId === id && nodeConnectionHandle.type === 'target') { + nodeConnectionHandle.position = targetPosition; + } + return nodeConnectionHandle; + } + ); + + setNodes((nodes: Node[]) => + nodes.map((node) => { + if (sourceNode.id === node.id) { + node.data = { ...node.data, connectionHandles: nodeSourceConnectionHandles }; + } + if (targetNode.id === node.id) { + node.data = { ...node.data, connectionHandles: nodeTargetConnectionHandles }; + } + return node; + }) + ); + } + } + }); + } + } + return change; + }); + }; + + return { onHandleChange }; +}; diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/handles/useHandleChange.types.ts b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/handles/useHandleChange.types.ts new file mode 100644 index 0000000000..1dc5d360ef --- /dev/null +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/handles/useHandleChange.types.ts @@ -0,0 +1,17 @@ +/******************************************************************************* + * Copyright (c) 2023 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +import { NodeChange } from 'reactflow'; + +export interface UseHandleChangeValue { + onHandleChange: (changes: NodeChange[]) => NodeChange[]; +} diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/handles/useRefreshConnectionHandles.tsx b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/handles/useRefreshConnectionHandles.tsx new file mode 100644 index 0000000000..c96f372f7d --- /dev/null +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/handles/useRefreshConnectionHandles.tsx @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) 2023 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ + +import { useEffect, useRef } from 'react'; +import { useUpdateNodeInternals } from 'reactflow'; +import { ConnectionHandle } from './ConnectionHandles.types'; +import { UseRefreshConnectionHandlesValue } from './useRefreshConnectionHandles.types'; + +export const useRefreshConnectionHandles = ( + id: string, + connectionHandles: ConnectionHandle[] +): UseRefreshConnectionHandlesValue => { + const updateNodeInternals = useUpdateNodeInternals(); + const firstUpdate = useRef(true); + + const connectionHandlesIdentity = connectionHandles + .map((handle) => `${handle.edgeId}#${handle.position}#${handle.nodeId}`) + .join(', '); + + useEffect(() => { + if (firstUpdate.current && firstUpdate) { + firstUpdate.current = false; + } else { + updateNodeInternals(id); + } + }, [connectionHandlesIdentity]); + + return { + useRefreshConnectionHandles, + }; +}; diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/handles/useRefreshConnectionHandles.types.ts b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/handles/useRefreshConnectionHandles.types.ts new file mode 100644 index 0000000000..8b2f03e546 --- /dev/null +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/handles/useRefreshConnectionHandles.types.ts @@ -0,0 +1,18 @@ +/******************************************************************************* + * Copyright (c) 2023 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ + +import { ConnectionHandle } from './ConnectionHandles.types'; + +export interface UseRefreshConnectionHandlesValue { + useRefreshConnectionHandles: (id: string, connectionHandles: ConnectionHandle[]) => void; +} diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/layout.tsx b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/layout.tsx index ff21f79a65..6051d8e652 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/layout.tsx +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/layout.tsx @@ -13,25 +13,26 @@ import { ApolloClient, InMemoryCache } from '@apollo/client/core'; import { ApolloProvider } from '@apollo/client/react'; -import { MessageOptions, ServerContext, theme, ToastContext } from '@eclipse-sirius/sirius-components-core'; +import { MessageOptions, ServerContext, ToastContext, theme } from '@eclipse-sirius/sirius-components-core'; import { ThemeProvider } from '@material-ui/core/styles'; import ELK, { ElkExtendedEdge, ElkLabel, ElkNode, LayoutOptions } from 'elkjs/lib/elk.bundled.js'; -import { createElement, Fragment } from 'react'; +import { Fragment, createElement } from 'react'; import ReactDOM from 'react-dom'; import { Edge, Node, ReactFlowProvider } from 'reactflow'; import { Diagram, NodeData } from '../DiagramRenderer.types'; -import { DiagramDirectEditContextProvider } from '../direct-edit/DiagramDirectEditContext'; import { Label } from '../Label'; +import { DiagramDirectEditContextProvider } from '../direct-edit/DiagramDirectEditContext'; import { IconLabelNodeData } from '../node/IconsLabelNode.types'; import { ListNode } from '../node/ListNode'; import { ListNodeData } from '../node/ListNode.types'; import { DiagramNodeType } from '../node/NodeTypes.types'; import { RectangularNode } from '../node/RectangularNode'; import { RectangularNodeData } from '../node/RectangularNode.types'; -import { isEastBorderNode, isWestBorderNode } from './layoutBorderNodes'; import { ReferencePosition } from './LayoutContext.types'; import { LayoutEngine } from './LayoutEngine'; import { ILayoutEngine, INodeLayoutHandler } from './LayoutEngine.types'; +import { isEastBorderNode, isWestBorderNode } from './layoutBorderNodes'; +import { layoutHandles } from './layoutHandles'; import { getChildren } from './layoutNode'; const emptyNodeProps = { @@ -265,6 +266,8 @@ const layoutDiagram = ( } } }); + + layoutHandles(diagram); }; export const performDefaultAutoLayout = ( diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/layoutHandles.ts b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/layoutHandles.ts new file mode 100644 index 0000000000..52205e3a8f --- /dev/null +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/layoutHandles.ts @@ -0,0 +1,66 @@ +/******************************************************************************* + * Copyright (c) 2023 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +import { Diagram } from '../DiagramRenderer.types'; +import { getEdgeParameters } from '../edge/EdgeLayout'; +import { ConnectionHandle } from '../handles/ConnectionHandles.types'; + +export const layoutHandles = (diagram: Diagram) => { + diagram.edges.forEach((edge) => { + const { sourceNode, targetNode, sourceHandle, targetHandle } = edge; + if (sourceNode && targetNode && sourceHandle && targetHandle) { + const { sourcePosition, targetPosition } = getEdgeParameters(sourceNode, targetNode, diagram.nodes); + + const nodeSourceConnectionHandle: ConnectionHandle | undefined = sourceNode.data.connectionHandles.find( + (connectionHandle: ConnectionHandle) => connectionHandle.id === sourceHandle + ); + const nodeTargetConnectionHandle: ConnectionHandle | undefined = targetNode.data.connectionHandles.find( + (connectionHandle: ConnectionHandle) => connectionHandle.id === targetHandle + ); + + if ( + nodeSourceConnectionHandle?.position !== sourcePosition && + nodeTargetConnectionHandle?.position !== targetPosition + ) { + const nodeSourceConnectionHandles: ConnectionHandle[] = sourceNode.data.connectionHandles.map( + (nodeConnectionHandle: ConnectionHandle) => { + if (nodeConnectionHandle.edgeId === edge.id && nodeConnectionHandle.type === 'source') { + nodeConnectionHandle.position = sourcePosition; + } + return nodeConnectionHandle; + } + ); + + const nodeTargetConnectionHandles: ConnectionHandle[] = targetNode.data.connectionHandles.map( + (nodeConnectionHandle: ConnectionHandle) => { + if (nodeConnectionHandle.edgeId === edge.id && nodeConnectionHandle.type === 'target') { + nodeConnectionHandle.position = targetPosition; + } + return nodeConnectionHandle; + } + ); + + diagram.nodes = diagram.nodes.map((node) => { + if (edge.sourceNode && edge.targetNode) { + if (edge.sourceNode.id === node.id) { + node.data = { ...node.data, connectionHandles: nodeSourceConnectionHandles }; + } + if (edge.targetNode.id === node.id) { + node.data = { ...node.data, connectionHandles: nodeTargetConnectionHandles }; + } + } + return node; + }); + } + } + }); +}; diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/node/ImageNode.tsx b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/node/ImageNode.tsx index 8c390e3697..63ae184caf 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/node/ImageNode.tsx +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/node/ImageNode.tsx @@ -14,13 +14,15 @@ import { ServerContext, ServerContextValue } from '@eclipse-sirius/sirius-components-core'; import { Theme, useTheme } from '@material-ui/core/styles'; import { memo, useContext } from 'react'; -import { Handle, NodeProps, NodeResizer, Position } from 'reactflow'; +import { NodeProps, NodeResizer } from 'reactflow'; import { BorderNodePositon } from '../DiagramRenderer.types'; import { Label } from '../Label'; import { useConnector } from '../connector/useConnector'; import { useDropNode } from '../dropNode/useDropNode'; import { ConnectionCreationHandles } from '../handles/ConnectionCreationHandles'; +import { ConnectionHandles } from '../handles/ConnectionHandles'; import { ConnectionTargetHandle } from '../handles/ConnectionTargetHandle'; +import { useRefreshConnectionHandles } from '../handles/useRefreshConnectionHandles'; import { DiagramElementPalette } from '../palette/DiagramElementPalette'; import { ImageNodeData } from './ImageNode.types'; @@ -61,12 +63,14 @@ const computeBorderRotation = (data: ImageNodeData): string | undefined => { return undefined; }; -export const ImageNode = memo(({ data, isConnectable, id, selected }: NodeProps) => { +export const ImageNode = memo(({ data, id, selected }: NodeProps) => { const theme = useTheme(); const { dropFeedbackStyleProvider } = useDropNode(); const { httpOrigin } = useContext(ServerContext); - const { onConnectionStartElementClick, newConnectionStyleProvider } = useConnector(); + const { newConnectionStyleProvider } = useConnector(); const rotation = computeBorderRotation(data); + + useRefreshConnectionHandles(id, data.connectionHandles); return ( <> : null} {selected ? : null} - - - - + ); }); diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/node/ListNode.tsx b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/node/ListNode.tsx index 1a41e0c32d..cb523f54f8 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/node/ListNode.tsx +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/node/ListNode.tsx @@ -14,13 +14,15 @@ import { getCSSColor } from '@eclipse-sirius/sirius-components-core'; import { Theme, useTheme } from '@material-ui/core/styles'; import { memo } from 'react'; -import { Handle, NodeProps, NodeResizer, Position } from 'reactflow'; +import { NodeProps, NodeResizer } from 'reactflow'; import { Label } from '../Label'; import { useConnector } from '../connector/useConnector'; import { useDrop } from '../drop/useDrop'; import { useDropNode } from '../dropNode/useDropNode'; import { ConnectionCreationHandles } from '../handles/ConnectionCreationHandles'; +import { ConnectionHandles } from '../handles/ConnectionHandles'; import { ConnectionTargetHandle } from '../handles/ConnectionTargetHandle'; +import { useRefreshConnectionHandles } from '../handles/useRefreshConnectionHandles'; import { DiagramElementPalette } from '../palette/DiagramElementPalette'; import { ListNodeData } from './ListNode.types'; @@ -45,16 +47,17 @@ const listNodeStyle = ( return listNodeStyle; }; -export const ListNode = memo(({ data, isConnectable, id, selected }: NodeProps) => { +export const ListNode = memo(({ data, id, selected }: NodeProps) => { const theme = useTheme(); const { onDrop, onDragOver } = useDrop(); - const { onConnectionStartElementClick, newConnectionStyleProvider } = useConnector(); + const { newConnectionStyleProvider } = useConnector(); const { dropFeedbackStyleProvider } = useDropNode(); const handleOnDrop = (event: React.DragEvent) => { onDrop(event, id); }; + useRefreshConnectionHandles(id, data.connectionHandles); return ( <> {data.nodeDescription?.userResizable && ( @@ -78,38 +81,7 @@ export const ListNode = memo(({ data, isConnectable, id, selected }: NodeProps : null} {selected ? : null} - - - - + ); diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/node/RectangularNode.tsx b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/node/RectangularNode.tsx index 4d55ecf4b3..696b48a4ab 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/node/RectangularNode.tsx +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/node/RectangularNode.tsx @@ -14,13 +14,15 @@ import { getCSSColor } from '@eclipse-sirius/sirius-components-core'; import { Theme, useTheme } from '@material-ui/core/styles'; import React, { memo } from 'react'; -import { Handle, NodeProps, NodeResizer, Position } from 'reactflow'; +import { NodeProps, NodeResizer } from 'reactflow'; import { Label } from '../Label'; import { useConnector } from '../connector/useConnector'; import { useDrop } from '../drop/useDrop'; import { useDropNode } from '../dropNode/useDropNode'; import { ConnectionCreationHandles } from '../handles/ConnectionCreationHandles'; +import { ConnectionHandles } from '../handles/ConnectionHandles'; import { ConnectionTargetHandle } from '../handles/ConnectionTargetHandle'; +import { useRefreshConnectionHandles } from '../handles/useRefreshConnectionHandles'; import { DiagramElementPalette } from '../palette/DiagramElementPalette'; import { RectangularNodeData } from './RectangularNode.types'; @@ -43,16 +45,17 @@ const rectangularNodeStyle = ( return rectangularNodeStyle; }; -export const RectangularNode = memo(({ data, isConnectable, id, selected }: NodeProps) => { +export const RectangularNode = memo(({ data, id, selected }: NodeProps) => { const theme = useTheme(); const { onDrop, onDragOver } = useDrop(); - const { onConnectionStartElementClick, newConnectionStyleProvider } = useConnector(); + const { newConnectionStyleProvider } = useConnector(); const { dropFeedbackStyleProvider } = useDropNode(); const handleOnDrop = (event: React.DragEvent) => { onDrop(event, id); }; + useRefreshConnectionHandles(id, data.connectionHandles); return ( <> {data.nodeDescription?.userResizable && ( @@ -76,38 +79,7 @@ export const RectangularNode = memo(({ data, isConnectable, id, selected }: Node {selected ? : null} {selected ? : null} - - - - + ); diff --git a/packages/sirius-web/frontend/sirius-web/src/nodes/EllipseNode.tsx b/packages/sirius-web/frontend/sirius-web/src/nodes/EllipseNode.tsx index 6eeabc3c8a..e252f75217 100644 --- a/packages/sirius-web/frontend/sirius-web/src/nodes/EllipseNode.tsx +++ b/packages/sirius-web/frontend/sirius-web/src/nodes/EllipseNode.tsx @@ -14,16 +14,18 @@ import { getCSSColor } from '@eclipse-sirius/sirius-components-core'; import { ConnectionCreationHandles, + ConnectionHandles, ConnectionTargetHandle, DiagramElementPalette, Label, useConnector, useDrop, useDropNode, + useRefreshConnectionHandles, } from '@eclipse-sirius/sirius-components-diagrams-reactflow'; import { Theme, useTheme } from '@material-ui/core/styles'; import React, { memo } from 'react'; -import { Handle, NodeProps, NodeResizer, Position } from 'reactflow'; +import { NodeProps, NodeResizer } from 'reactflow'; import { EllipseNodeData } from './EllipseNode.types'; const ellipseNodeStyle = ( @@ -54,13 +56,15 @@ const ellipseNodeStyle = ( export const EllipseNode = memo(({ data, isConnectable, id, selected }: NodeProps) => { const theme = useTheme(); const { onDrop, onDragOver } = useDrop(); - const { onConnectionStartElementClick, newConnectionStyleProvider } = useConnector(); + const { newConnectionStyleProvider } = useConnector(); const { dropFeedbackStyleProvider } = useDropNode(); const handleOnDrop = (event: React.DragEvent) => { onDrop(event, id); }; + useRefreshConnectionHandles(id, data.connectionHandles); + return ( <> : null} {selected ? : null} - - - - + ); diff --git a/packages/sirius-web/frontend/sirius-web/src/nodes/EllipseNodeConverterHandler.ts b/packages/sirius-web/frontend/sirius-web/src/nodes/EllipseNodeConverterHandler.ts index dc0ccc93b1..bf2c09c7f9 100644 --- a/packages/sirius-web/frontend/sirius-web/src/nodes/EllipseNodeConverterHandler.ts +++ b/packages/sirius-web/frontend/sirius-web/src/nodes/EllipseNodeConverterHandler.ts @@ -13,24 +13,27 @@ import { AlignmentMap, BorderNodePositon, - convertLabelStyle, + ConnectionHandle, + GQLEdge, GQLNode, GQLNodeDescription, GQLNodeStyle, GQLViewModifier, IConvertEngine, INodeConverterHandler, + convertHandles, + convertLabelStyle, } from '@eclipse-sirius/sirius-components-diagrams-reactflow'; import { Node, XYPosition } from 'reactflow'; import { EllipseNodeData, GQLEllipseNodeStyle } from './EllipseNode.types'; const defaultPosition: XYPosition = { x: 0, y: 0 }; - const toEllipseNode = ( gqlNode: GQLNode, gqlParentNode: GQLNode | null, nodeDescription: GQLNodeDescription | undefined, - isBorderNode: boolean + isBorderNode: boolean, + gqlEdges: GQLEdge[] ): Node => { const { targetObjectId, @@ -44,6 +47,7 @@ const toEllipseNode = ( labelEditable, } = gqlNode; + const connectionHandles: ConnectionHandle[] = convertHandles(gqlNode, gqlEdges); const data: EllipseNodeData = { targetObjectId, targetObjectLabel, @@ -63,6 +67,7 @@ const toEllipseNode = ( defaultWidth: gqlNode.defaultWidth, defaultHeight: gqlNode.defaultHeight, borderNodePosition: isBorderNode ? BorderNodePositon.EAST : null, + connectionHandles, labelEditable, }; @@ -125,10 +130,11 @@ export class EllipseNodeConverterHandler implements INodeConverterHandler { parentNode: GQLNode | null, isBorderNode: boolean, nodes: Node[], - nodeDescriptions: GQLNodeDescription[] + nodeDescriptions: GQLNodeDescription[], + gqlEdges: GQLEdge[] ) { const nodeDescription = nodeDescriptions.find((description) => description.id === gqlNode.descriptionId); - nodes.push(toEllipseNode(gqlNode, parentNode, nodeDescription, isBorderNode)); + nodes.push(toEllipseNode(gqlNode, parentNode, nodeDescription, isBorderNode, gqlEdges)); convertEngine.convertNodes(gqlNode.borderNodes ?? [], gqlNode, nodes, nodeDescriptions); convertEngine.convertNodes(gqlNode.childNodes ?? [], gqlNode, nodes, nodeDescriptions); }