Skip to content

Commit

Permalink
[2572] Use dynamic handles
Browse files Browse the repository at this point in the history
Bug: #2572
Signed-off-by: Michaël Charfadi <michael.charfadi@obeosoft.com>
  • Loading branch information
mcharfadi authored and pcdavid committed Nov 16, 2023
1 parent d627841 commit d9e3417
Show file tree
Hide file tree
Showing 26 changed files with 691 additions and 219 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.adoc
Expand Up @@ -152,7 +152,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

Expand Down
Expand Up @@ -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 {
Expand All @@ -29,6 +30,7 @@ export interface INodeConverterHandler {
handle(
convertEngine: IConvertEngine,
gqlNode: GQLNode<GQLNodeStyle>,
gqlEdges: GQLEdge[],
parentNode: GQLNode<GQLNodeStyle> | null,
isBorderNode: boolean,
nodes: Node[],
Expand Down
Expand Up @@ -12,16 +12,18 @@
*******************************************************************************/
import { Node, XYPosition } from 'reactflow';
import { GQLNodeDescription } from '../graphql/query/nodeDescriptionFragment.types';
import { GQLEdge } from '../graphql/subscription/edgeFragment.types';
import {
GQLIconLabelNodeStyle,
GQLNode,
GQLNodeStyle,
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 };

Expand All @@ -43,6 +45,8 @@ const toIconLabelNode = (
labelEditable,
} = gqlNode;

const connectionHandles: ConnectionHandle[] = [];

const data: IconLabelNodeData = {
targetObjectId,
targetObjectLabel,
Expand All @@ -60,6 +64,7 @@ const toIconLabelNode = (
defaultWidth: gqlNode.defaultWidth,
defaultHeight: gqlNode.defaultHeight,
labelEditable: labelEditable,
connectionHandles,
};

if (insideLabel) {
Expand Down Expand Up @@ -98,6 +103,7 @@ export class IconLabelNodeConverterHandler implements INodeConverterHandler {
handle(
_convertEngine: IConvertEngine,
gqlNode: GQLNode<GQLIconLabelNodeStyle>,
_gqlEdges: GQLEdge[],
parentNode: GQLNode<GQLNodeStyle> | null,
isBorderNode: boolean,
nodes: Node[],
Expand Down
Expand Up @@ -12,20 +12,24 @@
*******************************************************************************/
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 };

const toImageNode = (
gqlNode: GQLNode<GQLImageNodeStyle>,
gqlParentNode: GQLNode<GQLNodeStyle> | null,
nodeDescription: GQLNodeDescription | undefined,
isBorderNode: boolean
isBorderNode: boolean,
gqlEdges: GQLEdge[]
): Node<ImageNodeData> => {
const {
targetObjectId,
Expand All @@ -39,6 +43,8 @@ const toImageNode = (
labelEditable,
} = gqlNode;

const connectionHandles: ConnectionHandle[] = convertHandles(gqlNode, gqlEdges);

const data: ImageNodeData = {
targetObjectId,
targetObjectLabel,
Expand All @@ -55,6 +61,7 @@ const toImageNode = (
borderNodePosition: isBorderNode ? BorderNodePositon.WEST : null,
labelEditable,
positionDependentRotation: style.positionDependentRotation,
connectionHandles,
};

if (insideLabel) {
Expand Down Expand Up @@ -109,13 +116,14 @@ export class ImageNodeConverterHandler implements INodeConverterHandler {
handle(
convertEngine: IConvertEngine,
gqlNode: GQLNode<GQLImageNodeStyle>,
gqlEdges: GQLEdge[],
parentNode: GQLNode<GQLNodeStyle> | null,
isBorderNode: boolean,
nodes: Node[],
nodeDescriptions: GQLNodeDescription[]
) {
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);
}
Expand Down
Expand Up @@ -12,25 +12,29 @@
*******************************************************************************/
import { Node, XYPosition } from 'reactflow';
import { GQLNodeDescription } from '../graphql/query/nodeDescriptionFragment.types';
import { GQLEdge } from '../graphql/subscription/edgeFragment.types';
import {
GQLNode,
GQLNodeStyle,
GQLRectangularNodeStyle,
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 };

const toListNode = (
gqlNode: GQLNode<GQLRectangularNodeStyle>,
gqlParentNode: GQLNode<GQLNodeStyle> | null,
nodeDescription: GQLNodeDescription | undefined,
isBorderNode: boolean
isBorderNode: boolean,
gqlEdges: GQLEdge[]
): Node<ListNodeData> => {
const {
targetObjectId,
Expand All @@ -44,6 +48,8 @@ const toListNode = (
labelEditable,
} = gqlNode;

const connectionHandles: ConnectionHandle[] = convertHandles(gqlNode, gqlEdges);

const data: ListNodeData = {
targetObjectId,
targetObjectLabel,
Expand All @@ -62,6 +68,7 @@ const toListNode = (
faded: state === GQLViewModifier.Faded,
labelEditable,
nodeDescription,
connectionHandles,
defaultWidth: gqlNode.defaultWidth,
defaultHeight: gqlNode.defaultHeight,
};
Expand Down Expand Up @@ -123,13 +130,14 @@ export class ListNodeConverterHandler implements INodeConverterHandler {
handle(
convertEngine: IConvertEngine,
gqlNode: GQLNode<GQLRectangularNodeStyle>,
gqlEdges: GQLEdge[],
parentNode: GQLNode<GQLNodeStyle> | null,
isBorderNode: boolean,
nodes: Node[],
nodeDescriptions: GQLNodeDescription[]
) {
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);
}
Expand Down
Expand Up @@ -12,25 +12,29 @@
*******************************************************************************/
import { Node, XYPosition } from 'reactflow';
import { GQLNodeDescription } from '../graphql/query/nodeDescriptionFragment.types';
import { GQLEdge } from '../graphql/subscription/edgeFragment.types';
import {
GQLNode,
GQLNodeStyle,
GQLRectangularNodeStyle,
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 };

const toRectangularNode = (
gqlNode: GQLNode<GQLRectangularNodeStyle>,
gqlParentNode: GQLNode<GQLNodeStyle> | null,
nodeDescription: GQLNodeDescription | undefined,
isBorderNode: boolean
isBorderNode: boolean,
gqlEdges: GQLEdge[]
): Node<RectangularNodeData> => {
const {
targetObjectId,
Expand All @@ -44,6 +48,8 @@ const toRectangularNode = (
labelEditable,
} = gqlNode;

const connectionHandles: ConnectionHandle[] = convertHandles(gqlNode, gqlEdges);

const data: RectangularNodeData = {
targetObjectId,
targetObjectLabel,
Expand All @@ -65,6 +71,7 @@ const toRectangularNode = (
isBorderNode: isBorderNode,
borderNodePosition: isBorderNode ? BorderNodePositon.EAST : null,
labelEditable,
connectionHandles,
};

if (insideLabel) {
Expand Down Expand Up @@ -124,13 +131,14 @@ export class RectangleNodeConverterHandler implements INodeConverterHandler {
handle(
convertEngine: IConvertEngine,
gqlNode: GQLNode<GQLRectangularNodeStyle>,
gqlEdges: GQLEdge[],
parentNode: GQLNode<GQLNodeStyle> | null,
isBorderNode: boolean,
nodes: Node[],
nodeDescriptions: GQLNodeDescription[]
) {
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);
}
Expand Down
Expand Up @@ -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, gqlDiagram.edges, parentNode, isBorderNode, nodes, nodeDescriptions);
}
});
},
Expand All @@ -125,8 +125,10 @@ export const convertDiagram = (

const nodeId2Depth = new Map<string, number>();
nodes.forEach((node) => nodeId2Depth.set(node.id, nodeDepth(nodeId2node, node.id)));

let usedHandles: string[] = [];
const edges: Edge[] = gqlDiagram.edges.map((gqlEdge) => {
const sourceNode: Node<NodeData> | undefined = nodeId2node.get(gqlEdge.sourceId);
const targetNode: Node<NodeData> | undefined = nodeId2node.get(gqlEdge.targetId);
const data: MultiLabelEdgeData = {
targetObjectId: gqlEdge.targetObjectId,
targetObjectKind: gqlEdge.targetObjectKind,
Expand All @@ -145,6 +147,18 @@ export const convertDiagram = (
data.endLabel = convertEdgeLabel(gqlEdge.endLabel);
}

const sourceHandle = sourceNode?.data.connectionHandles
.filter((connectionHandle) => connectionHandle.type === 'source')
.find((connectionHandle) => !usedHandles.find((usedHandle) => usedHandle === connectionHandle.id));

const targetHandle = targetNode?.data.connectionHandles
.filter((connectionHandle) => connectionHandle.type === 'target')
.find((connectionHandle) => !usedHandles.find((usedHandle) => usedHandle === connectionHandle.id));

if (sourceHandle?.id && targetHandle?.id) {
usedHandles.push(sourceHandle?.id, targetHandle.id);
}

return {
id: gqlEdge.id,
type: 'multiLabelEdge',
Expand All @@ -159,6 +173,10 @@ export const convertDiagram = (
},
data,
hidden: gqlEdge.state === GQLViewModifier.Hidden,
sourceHandle: sourceHandle?.id,
targetHandle: targetHandle?.id,
sourceNode: sourceNode,
targetNode: targetNode,
};
});

Expand Down
@@ -0,0 +1,58 @@
/*******************************************************************************
* 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';

export const convertHandles = (gqlNode: GQLNode<GQLNodeStyle>, gqlEdges: GQLEdge[]): ConnectionHandle[] => {
const connectionHandles: ConnectionHandle[] = [];
let sourceHandlesCounter = 0;
let targetHandlesCounter = 0;
gqlEdges.forEach((edge) => {
if (edge.sourceId === gqlNode.id) {
connectionHandles.push({
id: `handle--source--${gqlNode.id}--${sourceHandlesCounter}`,
nodeId: gqlNode.id,
position: Position.Right,
type: 'source',
});
sourceHandlesCounter += 1;
}

if (edge.targetId === gqlNode.id) {
connectionHandles.push({
id: `handle--target--${gqlNode.id}--${targetHandlesCounter}`,
nodeId: gqlNode.id,
position: Position.Left,
type: 'target',
});
targetHandlesCounter += 1;
}
});
connectionHandles.push({
id: `handle--source--${gqlNode.id}--${sourceHandlesCounter}`,
nodeId: gqlNode.id,
position: Position.Right,
type: 'source',
});

connectionHandles.push({
id: `handle--target--${gqlNode.id}--${targetHandlesCounter}`,
nodeId: gqlNode.id,
position: Position.Left,
type: 'target',
});

return connectionHandles;
};
Expand Up @@ -16,17 +16,22 @@ 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';
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';
Expand Down

0 comments on commit d9e3417

Please sign in to comment.