Skip to content

Commit

Permalink
[2792] Add a first version for arrange all support
Browse files Browse the repository at this point in the history
Bug: #2792
Signed-off-by: Florian ROUËNÉ <florian.rouene@obeosoft.com>
  • Loading branch information
frouene authored and sbegaudeau committed Jan 11, 2024
1 parent fac807e commit afe7500
Show file tree
Hide file tree
Showing 10 changed files with 196 additions and 14 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ This default implementation will now also contain all the view models properly l
- [releng] The frontend now depends on react-trello 2.2.11
- https://github.com/eclipse-sirius/sirius-web/issues/2842[#2842] Move to @ObeoNetwork/react-trello
- [releng] Switch to EMFJson 2.3.6-SNAPSHOT
- [releng] The frontend now depends on elkjs 0.8.2


=== Bug fixes
Expand Down Expand Up @@ -122,6 +123,7 @@ Now, for a multi-valued feature, the values are properly displayed and the delet
- https://github.com/eclipse-sirius/sirius-web/issues/2883[#2883] [diagram] The size of manually resized node does not change anymore, unless it needs to.
+
The size of nodes that do not have been resized manually tries to fit the node default size.
- https://github.com/eclipse-sirius/sirius-web/issues/2792[#2792] [diagram] Add a first version for arrange all support.

=== Improvements

Expand Down
15 changes: 15 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 @@ -34,6 +34,7 @@
"@eclipse-sirius/sirius-components-core": "*",
"@material-ui/core": "4.12.4",
"@material-ui/icons": "4.11.3",
"elkjs": "0.8.2",
"graphql": "16.8.0",
"html-to-image": "1.11.11",
"react": "17.0.2",
Expand All @@ -51,6 +52,7 @@
"@types/react-dom": "17.0.9",
"@vitejs/plugin-react": "4.0.4",
"@vitest/coverage-v8": "0.34.2",
"elkjs": "0.8.2",
"graphql": "16.8.0",
"html-to-image": "1.11.11",
"jsdom": "16.7.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2023 Obeo.
* Copyright (c) 2023, 2024 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
Expand All @@ -19,3 +19,4 @@ export const borderTopAndBottom = 2;
export const borderNodeOffset = 5;
export const defaultWidth = 150;
export const defaultHeight = 70;
export const headerVerticalOffset = 25;
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*******************************************************************************
* Copyright (c) 2024 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 { UseArrangeAllValue } from './useArrangeAll.types';
import { Edge, Node, useReactFlow } from 'reactflow';
import { EdgeData, NodeData } from '../DiagramRenderer.types';
import { DiagramNodeType } from '../node/NodeTypes.types';
import { ListNodeData } from '../node/ListNode.types';
import ELK, { ElkNode } from 'elkjs/lib/elk.bundled';
import { LayoutOptions } from 'elkjs/lib/elk-api';
import { headerVerticalOffset } from './layoutParams';

const isListData = (node: Node): node is Node<ListNodeData> => node.type === 'listNode';

function reverseOrdreMap<K, V>(map: Map<K, V>): Map<K, V> {
const reversedNodes = Array.from(map.entries()).reverse();
return new Map<K, V>(reversedNodes);
}

const getSubNodes = (nodes: Node<NodeData, string>[]): Map<string, Node<NodeData, string>[]> => {
const subNodes: Map<string, Node<NodeData, string>[]> = new Map<string, Node<NodeData, string>[]>();
for (const node of nodes) {
const parentNodeId: string = node.parentNode ?? 'root';
if (!subNodes.has(parentNodeId)) {
subNodes.set(parentNodeId, []);
}
subNodes.get(parentNodeId)?.push(node);
}
return subNodes;
};

const elkOptions = {
'elk.algorithm': 'layered',
'elk.layered.spacing.nodeNodeBetweenLayers': '100',
'layering.strategy': 'NETWORK_SIMPLEX',
'elk.spacing.nodeNode': '100',
'elk.direction': 'RIGHT',
'elk.layered.spacing.edgeNodeBetweenLayers': '40',
};

export const useArrangeAll = (): UseArrangeAllValue => {
const { getNodes, getEdges, setNodes } = useReactFlow<NodeData, EdgeData>();
const elk = new ELK();

const getELKLayout = async (
nodes,
edges,
options: LayoutOptions = {},
parentNodeId: string,
withHeader: boolean
): Promise<any> => {
const graph: ElkNode = {
id: parentNodeId,
layoutOptions: options,
children: nodes.map((node) => ({
...node,
})),
edges,
};
try {
const layoutedGraph = await elk.layout(graph);
return {
nodes:
layoutedGraph?.children?.map((node_1) => ({
...node_1,
position: { x: node_1.x ?? 0, y: (node_1.y ?? 0) + (withHeader ? headerVerticalOffset : 0) },
})) ?? [],
layoutReturn: layoutedGraph,
};
} catch (message) {
return console.error(message);
}
};

const applyElkOnSubNodes = async (
subNodes: Map<string, Node<NodeData, string>[]>,
allNodes: Node<NodeData, string>[]
): Promise<Node<NodeData, string>[]> => {
let layoutedAllNodes: Node<NodeData, string>[] = [];
const parentNodeWithNewSize: Node<NodeData>[] = [];
const edges: Edge<EdgeData>[] = getEdges();
for (const [parentNodeId, nodes] of subNodes) {
const parentNode = allNodes.find((node) => node.id === parentNodeId);
if (parentNode && isListData(parentNode)) {
// No elk layout for child of container list
layoutedAllNodes = [...layoutedAllNodes, ...nodes];
continue;
}
const withHeader: boolean = parentNode?.data.insideLabel?.isHeader ?? false;
const subGroupNodes: Node<NodeData>[] = nodes
.filter((node) => !node.data.isBorderNode)
.map((node) => {
return parentNodeWithNewSize.find((layoutNode) => layoutNode.id === node.id) ?? node;
});
const subGroupEdges: Edge<EdgeData>[] = [];
edges.forEach((edge) => {
const isTargetInside = subGroupNodes.some((node) => node.id === edge.target);
const isSourceInside = subGroupNodes.some((node) => node.id === edge.source);
if (isTargetInside && isSourceInside) {
subGroupEdges.push(edge);
}
if (isTargetInside && !isSourceInside) {
edge.target = parentNodeId;
}
if (!isTargetInside && isSourceInside) {
edge.source = parentNodeId;
}
});
await getELKLayout(subGroupNodes, subGroupEdges, elkOptions, parentNodeId, withHeader).then(
({ nodes: layoutedSubNodes, layoutReturn }) => {
const parentNode = allNodes.find((node) => node.id === parentNodeId);
if (parentNode) {
parentNode.width = layoutReturn.width;
parentNode.height = layoutReturn.height + (withHeader ? headerVerticalOffset : 0);
parentNode.style = { width: `${parentNode.width}px`, height: `${parentNode.height}px` };
parentNodeWithNewSize.push(parentNode);
}
layoutedAllNodes = [
...layoutedAllNodes,
...layoutedSubNodes,
...nodes.filter((node) => node.data.isBorderNode),
];
}
);
}
return layoutedAllNodes;
};

const arrangeAll = (): void => {
const nodes: Node<NodeData, string>[] = [...getNodes()] as Node<NodeData, DiagramNodeType>[];
const subNodes: Map<string, Node<NodeData, string>[]> = reverseOrdreMap(getSubNodes(nodes));
applyElkOnSubNodes(subNodes, nodes).then((nodes: Node<NodeData, string>[]) => {
const reversedOrder: Node<NodeData, string>[] = nodes.reverse();
setNodes(reversedOrder);
});
};

return {
arrangeAll,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*******************************************************************************
* Copyright (c) 2024 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
*******************************************************************************/

export interface UseArrangeAllValue {
arrangeAll: () => void;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,11 @@

import { ServerContext, ServerContextValue } from '@eclipse-sirius/sirius-components-core';
import { useContext, useEffect, useState } from 'react';
import { Node, useReactFlow } from 'reactflow';
import { useReactFlow } from 'reactflow';
import { NodeTypeContext } from '../../contexts/NodeContext';
import { NodeTypeContextValue } from '../../contexts/NodeContext.types';
import { GQLReferencePosition } from '../../graphql/subscription/diagramEventSubscription.types';
import { EdgeData, NodeData } from '../DiagramRenderer.types';
import { DiagramNodeType } from '../node/NodeTypes.types';
import { cleanLayoutArea, layout, prepareLayoutArea } from './layout';
import { RawDiagram } from './layout.types';
import { UseLayoutState, UseLayoutValue } from './useLayout.types';
Expand Down Expand Up @@ -100,16 +99,7 @@ export const useLayout = (): UseLayoutValue => {
}
}, [state.currentStep, state.hiddenContainer, state.referencePosition]);

const arrangeAll = (afterLayoutCallback: (laidoutDiagram: RawDiagram) => void): void => {
const nodes = [...reactFlowInstance.getNodes()] as Node<NodeData, DiagramNodeType>[];
const diagramToLayout: RawDiagram = { edges: [...reactFlowInstance.getEdges()], nodes };
const previousDiagram: RawDiagram = { edges: [...reactFlowInstance.getEdges()], nodes: [] };

layoutDiagram(previousDiagram, diagramToLayout, null, afterLayoutCallback);
};

return {
layout: layoutDiagram,
arrangeAll,
};
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2023 Obeo.
* Copyright (c) 2023, 2024 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
Expand All @@ -21,7 +21,6 @@ export interface UseLayoutValue {
referencePosition: GQLReferencePosition | null,
callback: (laidoutDiagram: RawDiagram) => void
) => void;
arrangeAll: (afterLayoutCallback: (laidoutDiagram: RawDiagram) => void) => void;
}

export type Step = 'INITIAL_STEP' | 'BEFORE_LAYOUT' | 'LAYOUT' | 'AFTER_LAYOUT';
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 @@ -35,6 +36,7 @@ import { useHideDiagramElements } from '../hide/useHideDiagramElements';
import { usePinDiagramElements } from '../pin/usePinDiagramElements';
import { DiagramPanelProps, DiagramPanelState } from './DiagramPanel.types';
import { useExportToImage } from './useExportToImage';
import { useArrangeAll } from '../layout/useArrangeAll';

export const DiagramPanel = memo(({ snapToGrid, onSnapToGrid }: DiagramPanelProps) => {
const [state, setState] = useState<DiagramPanelState>({
Expand All @@ -48,6 +50,7 @@ export const DiagramPanel = memo(({ snapToGrid, onSnapToGrid }: DiagramPanelProp
const getSelectedNodes = () => getNodes().filter((node) => node.selected);

const { fullscreen, onFullscreen } = useFullscreen();
const { arrangeAll } = useArrangeAll();

const handleFitToScreen = () => reactFlow.fitView({ duration: 200, nodes: getSelectedNodes() });
const handleZoomIn = () => reactFlow.zoomIn({ duration: 200 });
Expand Down Expand Up @@ -133,6 +136,9 @@ export const DiagramPanel = memo(({ snapToGrid, onSnapToGrid }: DiagramPanelProp
<GridOnIcon />
</IconButton>
)}
<IconButton size="small" onClick={() => arrangeAll()}>
<AccountTreeIcon />
</IconButton>
<IconButton
size="small"
aria-label="reveal hidden elements"
Expand Down
1 change: 1 addition & 0 deletions packages/sirius-web/frontend/sirius-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@types/react-router-dom": "5.3.3",
"@xstate/react": "1.6.3",
"d3": "7.0.0",
"elkjs": "0.8.2",
"fontsource-roboto": "4.0.0",
"graphql": "16.8.0",
"html-to-image": "1.11.11",
Expand Down

0 comments on commit afe7500

Please sign in to comment.