Skip to content

Commit

Permalink
[2090] Add the support for arrange all with reactflow
Browse files Browse the repository at this point in the history
Bug: #2090
Signed-off-by: Guillaume Coutable <guillaume.coutable@obeo.fr>
  • Loading branch information
gcoutable committed Jun 28, 2023
1 parent 7181f25 commit 9bccdb6
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ An absent/empty candidates expression now simply means the widget is empty.
image:doc/screenshots/ShowIconOptionSelectWidget.jpg[Icons on select widget option,70%,30%]
- https://github.com/eclipse-sirius/sirius-components/issues/2055[#2055] [form] Added initial version of a custom widget to view & edit EMF references (both single and multi-valued).
- https://github.com/eclipse-sirius/sirius-components/issues/2056[#2056] [form] Add the possibility to control read-only mode for widgets with an expression.
- https://github.com/eclipse-sirius/sirius-components/issues/2090[#2090] [diagram] Add the support for the arrange all on react flow diagram. The arrange all is made with elkjs.

=== Improvements

Expand Down
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"@types/react-dom": "17.0.9",
"@vitejs/plugin-react": "2.0.0",
"c8": "7.12.0",
"elkjs": "0.8.2",
"jsdom": "16.7.0",
"graphql": "16.5.0",
"react": "17.0.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

import IconButton from '@material-ui/core/IconButton';
import Paper from '@material-ui/core/Paper';
import AccountTreeIcon from '@material-ui/icons/AccountTree';
import AspectRatioIcon from '@material-ui/icons/AspectRatio';
import FullscreenIcon from '@material-ui/icons/Fullscreen';
import FullscreenExitIcon from '@material-ui/icons/FullscreenExit';
Expand All @@ -34,6 +35,7 @@ export const DiagramPanel = ({
onZoomOut,
snapToGrid,
onSnapToGrid,
onArrangeAll,
}: DiagramPanelProps) => {
const [state, setState] = useState<DiagramPanelState>({
dialogOpen: null,
Expand Down Expand Up @@ -76,6 +78,9 @@ export const DiagramPanel = ({
<GridOnIcon />
</IconButton>
)}
<IconButton size="small" onClick={() => onArrangeAll()}>
<AccountTreeIcon />
</IconButton>
</Paper>
</Panel>
{state.dialogOpen === 'Share' ? <ShareDiagramDialog onClose={onCloseDialog} /> : null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface DiagramPanelProps {
onZoomOut: () => void;
snapToGrid: boolean;
onSnapToGrid: (snapToGrid: boolean) => void;
onArrangeAll: () => void;
}

export interface DiagramPanelState {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
*******************************************************************************/

import { Selection, SelectionEntry } from '@eclipse-sirius/sirius-components-core';
import { useEffect, useRef, useState } from 'react';
import { LayoutOptions } from 'elkjs/lib/elk.bundled.js';
import { useCallback, useEffect, useRef, useState } from 'react';
import {
Background,
BackgroundVariant,
Expand All @@ -35,6 +36,7 @@ import { RectangularNode } from './RectangularNode';

import 'reactflow/dist/style.css';
import { DiagramPanel } from './DiagramPanel';
import { performLayout } from './layout';

const nodeTypes: NodeTypes = {
rectangularNode: RectangularNode,
Expand Down Expand Up @@ -137,6 +139,20 @@ export const DiagramRenderer = ({ diagram, selection, setSelection }: DiagramRen
const handleZoomIn = () => reactFlow.zoomIn({ duration: 200 });
const handleZoomOut = () => reactFlow.zoomOut({ duration: 200 });
const handleSnapToGrid = (snapToGrid: boolean) => setState((prevState) => ({ ...prevState, snapToGrid }));
const handleArrangeAll = useCallback(() => {
const layoutOptions: LayoutOptions = {
'elk.algorithm': 'layered',
'elk.layered.spacing.nodeNodeBetweenLayers': '100',
'org.eclipse.elk.hierarchyHandling': 'INCLUDE_CHILDREN',
'layering.strategy': 'NETWORK_SIMPLEX',
'elk.spacing.nodeNode': '80',
'elk.direction': 'DOWN',
'elk.layered.spacing.edgeNodeBetweenLayers': '30',
};
performLayout(nodes, edges, layoutOptions).then(({ nodes }) => {
setNodes(nodes);
});
}, [nodes, edges]);

return (
<ReactFlow
Expand Down Expand Up @@ -173,6 +189,7 @@ export const DiagramRenderer = ({ diagram, selection, setSelection }: DiagramRen
onZoomOut={handleZoomOut}
snapToGrid={state.snapToGrid}
onSnapToGrid={handleSnapToGrid}
onArrangeAll={handleArrangeAll}
/>
</ReactFlow>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*******************************************************************************
* 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 ELK, { ElkExtendedEdge, ElkLabel, ElkNode, LayoutOptions } from 'elkjs/lib/elk.bundled.js';
import { Edge, Node } from 'reactflow';

const elk = new ELK();

export const performLayout = (
nodes: Node[],
edges: Edge[],
layoutOptions: LayoutOptions
): Promise<{ nodes: Node[] }> => {
const graph: ElkNode = {
id: 'root',
layoutOptions,
children: [],
edges: [],
};

const nodeId2Node = new Map<string, Node>();
nodes.forEach((node) => nodeId2Node.set(node.id, node));

const nodeId2ElkNode = new Map<String, ElkNode>();
nodes.forEach((node) => {
const elkNode: ElkNode = {
id: node.id,
children: [],
labels: [],
layoutOptions: {
// TODO: compute label placement from style (flex, ect...)
'nodeLabels.placement': '[H_CENTER, V_TOP, INSIDE]',
},
};
if (node.data?.label) {
const label = document.querySelector<HTMLDivElement>(`[data-id="${node.id}"] > div > div`);
const elkLabel: ElkLabel = {
width: label.getBoundingClientRect().width,
height: label.getBoundingClientRect().height,
text: node.data.label.text,
};

elkNode.labels.push(elkLabel);
}

const element = document.querySelector(`[data-id="${node.id}"]`);
if (element) {
elkNode.width = element.clientWidth;
elkNode.height = element.clientHeight;
}

nodeId2ElkNode.set(elkNode.id, elkNode);
});

nodes.forEach((node) => {
if (graph.children) {
if (!!node.parentNode) {
const elknodeChild = nodeId2ElkNode.get(node.id);
const elkNodeParent = nodeId2ElkNode.get(node.parentNode);
elkNodeParent.children.push(elknodeChild);
} else {
const elkNodeRoot = nodeId2ElkNode.get(node.id);
graph.children.push(elkNodeRoot);
}
}
});

edges.forEach((edge) => {
if (graph.edges) {
const elkEdge: ElkExtendedEdge = {
id: edge.id,
sources: [edge.source],
targets: [edge.target],
};
graph.edges.push(elkEdge);
}
});

return elk.layout(graph).then((layoutedGraph) => {
const nodes: Node[] = [];
elkToReactFlow(layoutedGraph.children ?? [], nodes, nodeId2Node);
return {
nodes,
};
});
};

const elkToReactFlow = (elkNodes: ElkNode[], nodes: Node[], nodeId2Node: Map<string, Node>) => {
elkNodes.map((elkNode) => {
const node = nodeId2Node.get(elkNode.id);
if (node) {
node.position.x = elkNode.x ?? 0;
node.position.y = elkNode.y ?? 0;
node.width = elkNode.width ?? 150;
node.height = elkNode.height ?? 70;
if (node.style) {
if (node.style.width) {
node.style.width = `${node.width}px`;
}
if (node.style.height) {
node.style.height = `${node.height}px`;
}
}
nodes.push(node);
}
if (elkNode?.children.length > 0) {
elkToReactFlow(elkNode.children, nodes, nodeId2Node);
}
});
};

0 comments on commit 9bccdb6

Please sign in to comment.