Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
Bug: #2796
Signed-off-by: Florian ROUËNÉ <florian.rouene@obeosoft.com>
  • Loading branch information
frouene committed Jan 4, 2024
1 parent a4bef3b commit 5608eb6
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 4 deletions.
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 @@ -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 @@ -24,15 +25,19 @@ import TonalityIcon from '@material-ui/icons/Tonality';
import VisibilityOffIcon from '@material-ui/icons/VisibilityOff';
import ZoomInIcon from '@material-ui/icons/ZoomIn';
import ZoomOutIcon from '@material-ui/icons/ZoomOut';
import { memo, useState } from 'react';
import { Panel, useReactFlow } from 'reactflow';
import { memo, useCallback, useState } from 'react';
import { Edge, Node, Panel, useReactFlow } from 'reactflow';
import { EdgeData, NodeData } from '../DiagramRenderer.types';
import { ShareDiagramDialog } from '../ShareDiagramDialog';
import { useFadeDiagramElements } from '../fade/useFadeDiagramElements';
import { useFullscreen } from '../fullscreen/useFullscreen';
import { useHideDiagramElements } from '../hide/useHideDiagramElements';
import { DiagramPanelProps, DiagramPanelState } from './DiagramPanel.types';
import { useExportToImage } from './useExportToImage';
import ELK, { ElkLabel, ElkNode } from 'elkjs/lib/elk.bundled.js';
import { ListNodeData } from '../node/ListNode.types';

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

export const DiagramPanel = memo(({ snapToGrid, onSnapToGrid }: DiagramPanelProps) => {
const [state, setState] = useState<DiagramPanelState>({
Expand All @@ -55,11 +60,139 @@ export const DiagramPanel = memo(({ snapToGrid, onSnapToGrid }: DiagramPanelProp

const { fadeDiagramElements } = useFadeDiagramElements();
const { hideDiagramElements } = useHideDiagramElements();

const elk = new ELK();
const onUnfadeAll = () => fadeDiagramElements([...getAllElementsIds()], false);
const onUnhideAll = () => hideDiagramElements([...getAllElementsIds()], false);

const { exportToImage } = useExportToImage();
const elkOptions = {
'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',
};
const getELKLayout = (nodes, edges, ports, options = {}, parentNode, parentNodeId: string): Promise<any> => {
const labels: ElkLabel[] = [];
if (parentNode && parentNode.data.label) {
const label = document.querySelector<HTMLDivElement>(`[data-id="${parentNode.data.label.id}"]`);
if (label) {
const elkLabel: ElkLabel = {
width: label.getBoundingClientRect().width,
height: label.getBoundingClientRect().height,
text: parentNode.data.label.text,
};
labels.push(elkLabel);
}
}
const graph: ElkNode = {
id: parentNodeId,
layoutOptions: options,
children: nodes.map((node) => ({
labels,
...node,
})),
edges,
ports: ports.map((port) => ({
...port,
})),
};
return elk
.layout(graph)
.then((layoutedGraph) => {
return {
nodes:
layoutedGraph?.children?.map((node) => ({
...node,
position: { x: node.x ?? 0, y: (node.y ?? 0) + 35 },
})) ?? [],
ports:
layoutedGraph?.ports?.map((port) => ({
...port,
position: { x: port.x, y: port.y },
})) ?? [],
layoutReturn: layoutedGraph,
};
})
.catch(console.error);
};

const applyElkOnSubNodes = async (subNodes: Map<string, Node<NodeData>[]>): Promise<Node<NodeData>[]> => {
let layoutedAllNodes: Node<NodeData>[] = [];
const layoutesSize: Node<NodeData>[] = [];
const edges: Edge<EdgeData>[] = reactFlow.getEdges();
for (const [parentNodeId, nodes] of subNodes) {
const parentNode = reactFlow.getNodes().find((node) => node.id === parentNodeId);
if (parentNode && isListData(parentNode)) {
layoutedAllNodes = [...layoutedAllNodes, ...nodes];
continue;
}
const subGroupNodes = nodes
.filter((node) => !node.data.isBorderNode)
.map((node) => {
const layoutedNode = layoutesSize.find((layoutNode) => layoutNode.id === node.id);
return layoutedNode ?? node;
});
const ports = nodes.filter((node) => node.data.isBorderNode);
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, ports, elkOptions, parentNode, parentNodeId).then(
({ nodes: layoutedNodes, ports: layoutedBorderNodes, layoutReturn }) => {
const parentNode = reactFlow.getNodes().find((node) => node.id === parentNodeId);
if (parentNode) {
parentNode.width = layoutReturn.width;
parentNode.height = layoutReturn.height + 35;
parentNode.style = { width: `${layoutReturn.width}px`, height: `${layoutReturn.height + 35}px` };
layoutesSize.push(parentNode);
}
layoutedAllNodes = [...layoutedAllNodes, ...layoutedNodes, ...layoutedBorderNodes];
}
);
}
return layoutedAllNodes;
};

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>[]): Map<string, Node<NodeData>[]> => {
const subNodes = new Map<string, Node<NodeData>[]>();

for (const node of nodes) {
const parentNodeId = node.parentNode ?? 'root';

if (!subNodes.has(parentNodeId)) {
subNodes.set(parentNodeId, []);
}
subNodes.get(parentNodeId)!.push(node);
}

return subNodes;
};

const handleArrangeAll = useCallback(() => {
const subNodes = reverseOrdreMap(getSubNodes(reactFlow.getNodes()));
applyElkOnSubNodes(subNodes).then((nodes) => {
const reversedOrder = nodes.reverse();
reactFlow.setNodes(reversedOrder);
});
}, []);

return (
<>
Expand Down Expand Up @@ -129,6 +262,9 @@ export const DiagramPanel = memo(({ snapToGrid, onSnapToGrid }: DiagramPanelProp
<GridOnIcon />
</IconButton>
)}
<IconButton size="small" onClick={() => handleArrangeAll()}>
<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 @@ -33,6 +33,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 5608eb6

Please sign in to comment.