Skip to content

Commit

Permalink
[2595] Call the layout on node move
Browse files Browse the repository at this point in the history
- Call the layout when the move has finished.
- The layout is called only if it is not a drop

Bug: #2595
  • Loading branch information
gcoutable committed Nov 24, 2023
1 parent 92ed029 commit 5932774
Show file tree
Hide file tree
Showing 16 changed files with 284 additions and 77 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.adoc
Expand Up @@ -175,7 +175,9 @@ The new implementation of `IEditService`, named `ComposedEditService`, tries fir
- https://github.com/eclipse-sirius/sirius-web/issues/2608[#2608] [diagram] Improve the placement of handles on the left and right side of nodes so that they are properly centered
- https://github.com/eclipse-sirius/sirius-web/issues/2262[#2262] [diagram] Share diagram layout data between the subscribers of the representation
- https://github.com/eclipse-sirius/sirius-web/issues/2621[#2621] [diagram] Set the same ConnectionLine path as the one used to render edges

- https://github.com/eclipse-sirius/sirius-web/issues/2595[#2595] [diagram] Call the layout on node move.
+
Node will not be outside of their container, nor on a node header because of a move.

== v2023.10.0

Expand Down
Expand Up @@ -25,6 +25,7 @@ import {
OnEdgesChange,
OnNodesChange,
ReactFlow,
applyNodeChanges,
useEdgesState,
useNodesState,
} from 'reactflow';
Expand All @@ -44,6 +45,7 @@ import { edgeTypes } from './edge/EdgeTypes';
import { MultiLabelEdgeData } from './edge/MultiLabelEdge.types';
import { useInitialFitToScreen } from './fit-to-screen/useInitialFitToScreen';
import { useHandleChange } from './handles/useHandleChange';
import { usePositionChange } from './layout-events/usePositionChange';
import { RawDiagram } from './layout/layout.types';
import { useLayout } from './layout/useLayout';
import { useSynchronizeLayoutData } from './layout/useSynchronizeLayoutData';
Expand Down Expand Up @@ -77,12 +79,11 @@ export const DiagramRenderer = ({ diagramRefreshedEventPayload, selection, setSe
const { onConnect, onConnectStart, onConnectEnd } = useConnector();
const { reconnectEdge } = useReconnectEdge();
const { onDrop, onDragOver } = useDrop();
const { onBorderChange } = useBorderChange();
const { onHandleChange } = useHandleChange();
const { getNodeTypes } = useNodeType();

const [nodes, setNodes, onNodesChange] = useNodesState<NodeData>([]);
const [edges, setEdges, onEdgesChange] = useEdgesState<MultiLabelEdgeData>([]);
// const { setNodes: setNodes2 } = useReactFlow<NodeData, EdgeData>();

const { nodeConverterHandlers } = useContext<NodeTypeContextValue>(NodeTypeContext);
const { fitToScreen } = useInitialFitToScreen();
Expand Down Expand Up @@ -121,9 +122,19 @@ export const DiagramRenderer = ({ diagramRefreshedEventPayload, selection, setSe
}, [diagramRefreshedEventPayload, diagramDescription]);

const { updateSelectionOnNodesChange, updateSelectionOnEdgesChange } = useDiagramSelection(selection, setSelection);
const { transformBorderNodeChanges } = useBorderChange();
const { applyHandleChange } = useHandleChange();
const { onPositionChange } = usePositionChange();

const handleNodesChange: OnNodesChange = (changes: NodeChange[]) => {
onNodesChange(onBorderChange(onHandleChange(changes)));
const transformedNodeChanges = transformBorderNodeChanges(changes);

let newNodes = applyNodeChanges(transformedNodeChanges, nodes);

newNodes = applyHandleChange(transformedNodeChanges, newNodes as Node<NodeData, DiagramNodeType>[]);
setNodes(newNodes);
onPositionChange(transformedNodeChanges, newNodes as Node<NodeData, DiagramNodeType>[]);

updateSelectionOnNodesChange(changes);
};

Expand Down
Expand Up @@ -65,7 +65,7 @@ const computeNewBorderPosition = (
export const useBorderChange = (): UseBorderChangeValue => {
const { getNodes } = useReactFlow<NodeData, EdgeData>();

const onBorderChange = useCallback((changes: NodeChange[]): NodeChange[] => {
const transformBorderNodeChanges = useCallback((changes: NodeChange[]): NodeChange[] => {
return changes.map((change) => {
if (change.type === 'position' && change.positionAbsolute) {
const movedNode = getNodes().find((node) => change.id === node.id);
Expand All @@ -88,5 +88,5 @@ export const useBorderChange = (): UseBorderChangeValue => {
});
}, []);

return { onBorderChange };
return { transformBorderNodeChanges };
};
Expand Up @@ -13,5 +13,5 @@
import { NodeChange } from 'reactflow';

export interface UseBorderChangeValue {
onBorderChange: (changes: NodeChange[]) => NodeChange[];
transformBorderNodeChanges: (changes: NodeChange[]) => NodeChange[];
}
Expand Up @@ -212,6 +212,10 @@ export const useDropNode = (): UseDropNodeValue => {
[element?.top, element?.left, viewport, draggedNode, targetNodeId, droppableOnDiagram, compatibleNodeIds]
);

const hasDroppedNodeParentChanged = (): boolean => {
return draggedNode?.parentNode !== targetNodeId;
};

const theme = useTheme();
const diagramForbidden = draggedNode?.id !== null && !droppableOnDiagram;
const diagramTargeted = targetNodeId === null && initialParentId !== null;
Expand All @@ -227,6 +231,7 @@ export const useDropNode = (): UseDropNodeValue => {
onNodeDragStart,
onNodeDrag,
onNodeDragStop,
hasDroppedNodeParentChanged,
compatibleNodeIds,
draggedNode,
targetNodeId,
Expand Down
Expand Up @@ -19,6 +19,7 @@ export interface UseDropNodeValue {
onNodeDragStart: NodeDragHandler;
onNodeDrag: NodeDragHandler;
onNodeDragStop: (onDragCancelled: (node: Node) => void) => NodeDragHandler;
hasDroppedNodeParentChanged: () => boolean;
draggedNode: Node<NodeData> | null;
targetNodeId: string | null;
compatibleNodeIds: string[];
Expand Down
Expand Up @@ -84,8 +84,8 @@ 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,
x: (movingNode.positionAbsolute?.x ?? 0) + (nodeA.width ?? 0) / 2,
y: (movingNode.positionAbsolute?.y ?? 0) + (nodeA.height ?? 0) / 2,
};
} else {
centerA = getNodeCenter(nodeA, visiblesNodes);
Expand All @@ -94,17 +94,17 @@ const getParameters: GetParameters = (movingNode, nodeA, nodeB, 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,
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 horizontalDifference = Math.abs(centerA.x - centerB.x);
const verticalDifference = Math.abs(centerA.y - centerB.y);

let position: Position;
if (horizontallDifference > verticalDifference) {
if (horizontalDifference > verticalDifference) {
position = centerA.x > centerB.x ? Position.Left : Position.Right;
} else {
position = centerA.y > centerB.y ? Position.Top : Position.Bottom;
Expand All @@ -118,14 +118,14 @@ const getParameters: GetParameters = (movingNode, nodeA, nodeB, visiblesNodes) =
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,
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,
x: (node.position?.x ?? 0) + (node.width ?? 0) / 2,
y: (node.position?.y ?? 0) + (node.height ?? 0) / 2,
};
while (parentNode) {
position = {
Expand Down
Expand Up @@ -10,72 +10,74 @@
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
import { Node, NodeChange, getConnectedEdges, useReactFlow } from 'reactflow';
import { Node, NodeChange, NodePositionChange, getConnectedEdges, useReactFlow } from 'reactflow';
import { EdgeData, NodeData } from '../DiagramRenderer.types';
import { getEdgeParametersWhileMoving, getUpdatedConnectionHandles } from '../edge/EdgeLayout';
import { DiagramNodeType } from '../node/NodeTypes.types';
import { ConnectionHandle } from './ConnectionHandles.types';
import { UseHandleChangeValue } from './useHandleChange.types';

const isNodePositionChange = (change: NodeChange): change is NodePositionChange =>
change.type === 'position' && typeof change.dragging === 'boolean' && change.dragging;
export const useHandleChange = (): UseHandleChangeValue => {
const { getEdges, getNodes, setNodes } = useReactFlow<NodeData, EdgeData>();
const { getEdges } = useReactFlow<NodeData, EdgeData>();

const applyHandleChange = (
changes: NodeChange[],
nodes: Node<NodeData, DiagramNodeType>[]
): Node<NodeData, DiagramNodeType>[] => {
return nodes.map((node) => {
const nodeDraggingChange: NodePositionChange | undefined = changes
.filter(isNodePositionChange)
.find((change) => change.id === node.id);

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 (nodeDraggingChange) {
const connectedEdges = getConnectedEdges([node], getEdges());
connectedEdges.forEach((edge) => {
const { sourceHandle, targetHandle } = edge;
const sourceNode = nodes.find((node) => node.id === edge.sourceNode?.id);
const targetNode = nodes.find((node) => node.id === edge.targetNode?.id);

if (movedNode) {
const connectedEdges = getConnectedEdges([movedNode], getEdges());
connectedEdges.forEach((edge) => {
const { sourceHandle, targetHandle } = edge;
const sourceNode = getNodes().find((node) => node.id === edge.sourceNode?.id);
const targetNode = getNodes().find((node) => node.id === edge.targetNode?.id);
if (sourceNode && targetNode && sourceHandle && targetHandle) {
const { sourcePosition, targetPosition } = getEdgeParametersWhileMoving(
nodeDraggingChange,
sourceNode,
targetNode,
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 (sourceNode && targetNode && sourceHandle && targetHandle) {
const { sourcePosition, targetPosition } = getEdgeParametersWhileMoving(
change,
if (
nodeSourceConnectionHandle?.position !== sourcePosition &&
nodeTargetConnectionHandle?.position !== targetPosition
) {
const { sourceConnectionHandles, targetConnectionHandles } = getUpdatedConnectionHandles(
sourceNode,
targetNode,
getNodes()
);
const nodeSourceConnectionHandle: ConnectionHandle | undefined = sourceNode.data.connectionHandles.find(
(connectionHandle: ConnectionHandle) => connectionHandle.id === sourceHandle
sourcePosition,
targetPosition,
sourceHandle,
targetHandle
);
const nodeTargetConnectionHandle: ConnectionHandle | undefined = targetNode.data.connectionHandles.find(
(connectionHandle: ConnectionHandle) => connectionHandle.id === targetHandle
);

if (
nodeSourceConnectionHandle?.position !== sourcePosition &&
nodeTargetConnectionHandle?.position !== targetPosition
) {
const { sourceConnectionHandles, targetConnectionHandles } = getUpdatedConnectionHandles(
sourceNode,
targetNode,
sourcePosition,
targetPosition,
sourceHandle,
targetHandle
);

setNodes((nodes: Node<NodeData>[]) =>
nodes.map((node) => {
if (sourceNode.id === node.id) {
node.data = { ...node.data, connectionHandles: sourceConnectionHandles };
}
if (targetNode.id === node.id) {
node.data = { ...node.data, connectionHandles: targetConnectionHandles };
}
return node;
})
);
if (node.id === sourceNode.id) {
node.data = { ...node.data, connectionHandles: sourceConnectionHandles };
}
if (node.id === targetNode.id) {
node.data = { ...node.data, connectionHandles: targetConnectionHandles };
}
}
});
}
}
});
}
return change;
return node;
});
};

return { onHandleChange };
return { applyHandleChange };
};
Expand Up @@ -10,8 +10,13 @@
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
import { NodeChange } from 'reactflow';
import { Node, NodeChange } from 'reactflow';
import { NodeData } from '../DiagramRenderer.types';
import { DiagramNodeType } from '../node/NodeTypes.types';

export interface UseHandleChangeValue {
onHandleChange: (changes: NodeChange[]) => NodeChange[];
applyHandleChange: (
changes: NodeChange[],
nodes: Node<NodeData, DiagramNodeType>[]
) => Node<NodeData, DiagramNodeType>[];
}
@@ -0,0 +1,75 @@
/*******************************************************************************
* 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, useReactFlow } from 'reactflow';
import { EdgeData, NodeData } from '../DiagramRenderer.types';
import { useDropNode } from '../dropNode/useDropNode';
import { RawDiagram } from '../layout/layout.types';
import { useLayout } from '../layout/useLayout';
import { DiagramNodeType } from '../node/NodeTypes.types';
import { UsePositionChangeValue } from './usePositionChange.types';

export const usePositionChange = (): UsePositionChangeValue => {
const { getEdges, setNodes, setEdges } = useReactFlow<NodeData, EdgeData>();
const { layout } = useLayout();
const { hasDroppedNodeParentChanged } = useDropNode();

const isMoveFinished = (change: NodeChange): boolean => {
return (
change.type === 'position' &&
typeof change.dragging === 'boolean' &&
!change.dragging &&
!hasDroppedNodeParentChanged()
);
};
const isResizeFinished = (change: NodeChange): boolean =>
change.type === 'dimensions' && typeof change.resizing === 'boolean' && !change.resizing;

const findNeededLayoutChange = (changes: NodeChange[]): NodeChange | undefined => {
return changes.find((change) => isMoveFinished(change) || isResizeFinished(change));
};

const onPositionChange = (changes: NodeChange[], nodes: Node<NodeData, DiagramNodeType>[]): void => {
const change = findNeededLayoutChange(changes);
if (change) {
const diagramToLayout: RawDiagram = {
nodes,
edges: getEdges(),
};

layout(diagramToLayout, diagramToLayout, (laidOutDiagram) => {
nodes.map((node) => {
const existingNode = laidOutDiagram.nodes.find((laidoutNode) => laidoutNode.id === node.id);
if (existingNode) {
return {
...node,
position: existingNode.position,
width: existingNode.width,
height: existingNode.height,
style: {
...node.style,
width: `${existingNode.width}px`,
height: `${existingNode.height}px`,
},
};
}
return node;
});
setNodes(nodes);
setEdges(laidOutDiagram.edges);
});
}
};

return { onPositionChange };
};

0 comments on commit 5932774

Please sign in to comment.