Skip to content

Commit

Permalink
[3354] Add support for overlapping nodes in arrangeAll and distribute…
Browse files Browse the repository at this point in the history
… elements

Bug: #3354
Signed-off-by: Florian ROUËNÉ <florian.rouene@obeosoft.com>
  • Loading branch information
frouene authored and sbegaudeau committed Apr 29, 2024
1 parent d8ceefd commit e6d5bfd
Show file tree
Hide file tree
Showing 10 changed files with 528 additions and 377 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ image:doc/screenshots/inside_outside_labels.png[Isinde outside label example, 70
- https://github.com/eclipse-sirius/sirius-web/issues/3382[#3382] [diagram] Add a tool section in Node and Edge palettes with tools to hide/show/reset elements and their children.
- https://github.com/eclipse-sirius/sirius-web/issues/3135[#3135] [diagram] Add new overflow strategies for node label : _wrap_ and _ellipsis_
- https://github.com/eclipse-sirius/sirius-web/issues/3425[#3425] [diagram] Make the direction applied during _arrangeAll_ configurable.
- https://github.com/eclipse-sirius/sirius-web/issues/3354[#3354] [diagram] Add support for overlapping nodes in arrangeAll and distribute elements

=== Improvements

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -345,11 +345,9 @@ export const DiagramRenderer = ({ diagramRefreshedEventPayload }: DiagramRendere
onSnapToGrid={onSnapToGrid}
helperLines={helperLinesEnabled}
onHelperLines={setHelperLinesEnabled}
refreshEventPayloadId={diagramRefreshedEventPayload.id}
reactFlowWrapper={ref}
/>
<GroupPalette
refreshEventPayloadId={diagramRefreshedEventPayload.id}
x={groupPalettePosition?.x}
y={groupPalettePosition?.y}
isOpened={isGroupPaletteOpened}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,22 @@
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
import { UseArrangeAllValue } from './useArrangeAll.types';
import { LayoutOptions } from 'elkjs/lib/elk-api';
import ELK, { ElkLabel, ElkNode } from 'elkjs/lib/elk.bundled';
import { useContext } from 'react';
import { Edge, Node, useReactFlow, useViewport } from 'reactflow';
import { DiagramContext } from '../../contexts/DiagramContext';
import { DiagramContextValue } from '../../contexts/DiagramContext.types';
import { useDiagramDescription } from '../../contexts/useDiagramDescription';
import { EdgeData, NodeData } from '../DiagramRenderer.types';
import { DiagramNodeType } from '../node/NodeTypes.types';
import { ListNodeData } from '../node/ListNode.types';
import ELK, { ElkLabel, ElkNode } from 'elkjs/lib/elk.bundled';
import { LayoutOptions } from 'elkjs/lib/elk-api';
import { labelVerticalPadding, labelHorizontalPadding } from './layoutParams';
import { DiagramNodeType } from '../node/NodeTypes.types';
import { useOverlap } from '../overlap/useOverlap';
import { RawDiagram } from './layout.types';
import { labelHorizontalPadding, labelVerticalPadding } from './layoutParams';
import { UseArrangeAllValue } from './useArrangeAll.types';
import { useLayout } from './useLayout';
import { useSynchronizeLayoutData } from './useSynchronizeLayoutData';
import { useDiagramDescription } from '../../contexts/useDiagramDescription';

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

Expand Down Expand Up @@ -110,15 +114,14 @@ const computeLabels = (
return labels;
};

export const useArrangeAll = (
refreshEventPayloadId: string,
reactFlowWrapper: React.MutableRefObject<HTMLDivElement | null>
): UseArrangeAllValue => {
export const useArrangeAll = (reactFlowWrapper: React.MutableRefObject<HTMLDivElement | null>): UseArrangeAllValue => {
const { getNodes, getEdges, setNodes, setEdges } = useReactFlow<NodeData, EdgeData>();
const viewport = useViewport();
const { layout } = useLayout();
const { synchronizeLayoutData } = useSynchronizeLayoutData();
const { diagramDescription } = useDiagramDescription();
const { refreshEventPayloadId } = useContext<DiagramContextValue>(DiagramContext);
const { resolveNodeOverlap } = useOverlap();

const elk = new ELK();

Expand All @@ -142,10 +145,17 @@ export const useArrangeAll = (
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) + headerVerticalFootprint },
})) ?? [],
layoutedGraph?.children?.map((node) => {
const originalNode = nodes.find((node_1) => node_1.id === node.id);
if (originalNode && originalNode.data.pinned) {
return { ...node };
} else {
return {
...node,
position: { x: node.x ?? 0, y: (node.y ?? 0) + headerVerticalFootprint },
};
}
}) ?? [],
layoutReturn: layoutedGraph,
};
} catch (message) {
Expand Down Expand Up @@ -234,7 +244,11 @@ export const useArrangeAll = (

layout(diagramToLayout, diagramToLayout, null, (laidOutDiagram) => {
laidOutNodesWithElk.map((node) => {
const existingNode = laidOutDiagram.nodes.find((laidOutNode) => laidOutNode.id === node.id);
const overlapFreeLaidOutNodes: Node<NodeData, string>[] = resolveNodeOverlap(
laidOutDiagram.nodes,
'horizontal'
) as Node<NodeData, DiagramNodeType>[];
const existingNode = overlapFreeLaidOutNodes.find((laidOutNode) => laidOutNode.id === node.id);
if (existingNode) {
return {
...node,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
import { useCallback } from 'react';
import { useCallback, useContext } from 'react';
import { Node, XYPosition, useReactFlow } from 'reactflow';
import { UseDistributeElementsValue } from './useDistributeElements.types';
import { NodeData, EdgeData } from '../DiagramRenderer.types';
Expand All @@ -19,6 +19,9 @@ import { useSynchronizeLayoutData } from './useSynchronizeLayoutData';
import { RawDiagram } from './layout.types';
import { useLayout } from './useLayout';
import { useMultiToast } from '@eclipse-sirius/sirius-components-core';
import { DiagramContextValue } from '../../contexts/DiagramContext.types';
import { DiagramContext } from '../../contexts/DiagramContext';
import { useOverlap } from '../overlap/useOverlap';

function getComparePositionFn(direction: 'horizontal' | 'vertical') {
return (node1: Node, node2: Node) => {
Expand All @@ -32,17 +35,20 @@ function getComparePositionFn(direction: 'horizontal' | 'vertical') {
}

const arrangeGapBetweenElements: number = 32;
export const useDistributeElements = (refreshEventPayloadId: string): UseDistributeElementsValue => {
export const useDistributeElements = (): UseDistributeElementsValue => {
const { getNodes, getEdges, setNodes } = useReactFlow<NodeData, EdgeData>();
const { layout } = useLayout();
const { synchronizeLayoutData } = useSynchronizeLayoutData();
const { addMessages } = useMultiToast();
const { refreshEventPayloadId } = useContext<DiagramContextValue>(DiagramContext);
const { resolveNodeOverlap } = useOverlap();

const processLayoutTool = (
selectedNodeIds: string[],
layoutFn: (selectedNodes: Node<NodeData>[], refNode: Node) => Node<NodeData>[],
sortFn: ((node1: Node, node2: Node) => number) | null = null,
refElementId: string | null = null
refElementId: string | null = null,
direction: 'horizontal' | 'vertical' = 'horizontal'
): void => {
const selectedNodes: Node<NodeData>[] = getNodes().filter((node) => selectedNodeIds.includes(node.id));
const firstParent = selectedNodes[0]?.parentNode;
Expand All @@ -67,14 +73,16 @@ export const useDistributeElements = (refreshEventPayloadId: string): UseDistrib
}
if (refNode) {
const updatedNodes: Node<NodeData>[] = layoutFn(selectedNodes, refNode);
const overlapFreeNodes: Node[] = resolveNodeOverlap(updatedNodes, direction);
const diagramToLayout: RawDiagram = {
nodes: [...updatedNodes] as Node<NodeData, DiagramNodeType>[],
nodes: [...overlapFreeNodes] as Node<NodeData, DiagramNodeType>[],
edges: getEdges(),
};
layout(diagramToLayout, diagramToLayout, null, (laidOutDiagram) => {
setNodes(laidOutDiagram.nodes);
const overlapFreeNodesAfterLayout: Node[] = resolveNodeOverlap(laidOutDiagram.nodes, 'horizontal');
setNodes(overlapFreeNodesAfterLayout);
const finalDiagram: RawDiagram = {
nodes: laidOutDiagram.nodes,
nodes: overlapFreeNodesAfterLayout as Node<NodeData, DiagramNodeType>[],
edges: laidOutDiagram.edges,
};
synchronizeLayoutData(refreshEventPayloadId, finalDiagram);
Expand Down Expand Up @@ -137,85 +145,98 @@ export const useDistributeElements = (refreshEventPayloadId: string): UseDistrib
},
};
});

setNodes(updatedNodes);
const finalDiagram: RawDiagram = {
nodes: [...updatedNodes] as Node<NodeData, DiagramNodeType>[],
const overlapFreeNodes: Node[] = resolveNodeOverlap(updatedNodes, direction);
const diagramToLayout: RawDiagram = {
nodes: [...overlapFreeNodes] as Node<NodeData, DiagramNodeType>[],
edges: getEdges(),
};
synchronizeLayoutData(refreshEventPayloadId, finalDiagram);
layout(diagramToLayout, diagramToLayout, null, (laidOutDiagram) => {
setNodes(laidOutDiagram.nodes);
const finalDiagram: RawDiagram = {
nodes: laidOutDiagram.nodes,
edges: laidOutDiagram.edges,
};
synchronizeLayoutData(refreshEventPayloadId, finalDiagram);
});
}
}, []);
};

const distributeAlign = (orientation: 'left' | 'right' | 'top' | 'bottom' | 'center' | 'middle') => {
return useCallback((selectedNodeIds: string[], refElementId: string | null) => {
processLayoutTool(
selectedNodeIds,
(_selectedNodes, refNode) => {
return getNodes().map((node) => {
if (!selectedNodeIds.includes(node.id) || node.data.pinned) {
return node;
}
const referencePositionValue: number = (() => {
switch (orientation) {
case 'left':
return refNode.position.x;
case 'right':
return refNode.position.x + (refNode.width ?? 0) - (node.width ?? 0);
case 'center':
return refNode.position.x + (refNode.width ?? 0) / 2 - (node.width ?? 0) / 2;
case 'top':
return refNode.position.y;
case 'bottom':
return refNode.position.y + (refNode.height ?? 0) - (node.height ?? 0);
case 'middle':
return refNode.position.y + (refNode.height ?? 0) / 2 - (node.height ?? 0) / 2;
return useCallback(
(selectedNodeIds: string[], refElementId: string | null) => {
processLayoutTool(
selectedNodeIds,
(_selectedNodes, refNode) => {
return getNodes().map((node) => {
if (!selectedNodeIds.includes(node.id) || node.data.pinned) {
return node;
}
})();
const referencePositionValue: number = (() => {
switch (orientation) {
case 'left':
return refNode.position.x;
case 'right':
return refNode.position.x + (refNode.width ?? 0) - (node.width ?? 0);
case 'center':
return refNode.position.x + (refNode.width ?? 0) / 2 - (node.width ?? 0) / 2;
case 'top':
return refNode.position.y;
case 'bottom':
return refNode.position.y + (refNode.height ?? 0) - (node.height ?? 0);
case 'middle':
return refNode.position.y + (refNode.height ?? 0) / 2 - (node.height ?? 0) / 2;
}
})();

const referencePositionVariable: string = (() => {
switch (orientation) {
case 'left':
case 'right':
case 'center':
return 'x';
case 'top':
case 'bottom':
case 'middle':
return 'y';
}
})();
const referencePositionVariable: string = (() => {
switch (orientation) {
case 'left':
case 'right':
case 'center':
return 'x';
case 'top':
case 'bottom':
case 'middle':
return 'y';
}
})();

return {
...node,
position: {
...node.position,
[referencePositionVariable]: referencePositionValue,
},
};
});
},
null,
refElementId
);
}, []);
return {
...node,
position: {
...node.position,
[referencePositionVariable]: referencePositionValue,
},
};
});
},
null,
refElementId,
['left', 'right', 'center'].includes(orientation) ? 'vertical' : 'horizontal'
);
},
[resolveNodeOverlap]
);
};

const justifyElements = (
justifyElementsFn: (selectedNodes: Node[], selectedNodeIds: string[], refNode: Node) => Node[]
) => {
return useCallback((selectedNodeIds: string[], refElementId: string | null) => {
processLayoutTool(
selectedNodeIds,
(selectedNodes, refNode) => {
selectedNodes.sort(getComparePositionFn('horizontal'));
return justifyElementsFn(selectedNodes, selectedNodeIds, refNode);
},
null,
refElementId
);
}, []);
return useCallback(
(selectedNodeIds: string[], refElementId: string | null) => {
processLayoutTool(
selectedNodeIds,
(selectedNodes, refNode) => {
selectedNodes.sort(getComparePositionFn('horizontal'));
return justifyElementsFn(selectedNodes, selectedNodeIds, refNode);
},
null,
refElementId
);
},
[resolveNodeOverlap]
);
};

const justifyHorizontally = justifyElements(
Expand Down Expand Up @@ -332,7 +353,9 @@ export const useDistributeElements = (refreshEventPayloadId: string): UseDistrib
return node;
});
},
getComparePositionFn('vertical')
getComparePositionFn('vertical'),
null,
'vertical'
);
};

Expand Down Expand Up @@ -389,30 +412,33 @@ export const useDistributeElements = (refreshEventPayloadId: string): UseDistrib
const distributeAlignBottom = distributeAlign('bottom');
const distributeAlignMiddle = distributeAlign('middle');

const makeNodesSameSize = useCallback((selectedNodeIds: string[], refElementId: string | null) => {
processLayoutTool(
selectedNodeIds,
(_selectedNodes, refNode) => {
return getNodes().map((node) => {
if (!selectedNodeIds.includes(node.id) || node.data.nodeDescription?.userResizable === false) {
return node;
}
const makeNodesSameSize = useCallback(
(selectedNodeIds: string[], refElementId: string | null) => {
processLayoutTool(
selectedNodeIds,
(_selectedNodes, refNode) => {
return getNodes().map((node) => {
if (!selectedNodeIds.includes(node.id) || node.data.nodeDescription?.userResizable === false) {
return node;
}

return {
...node,
width: refNode.width,
height: refNode.height,
data: {
...node.data,
resizedByUser: true,
},
};
});
},
null,
refElementId
);
}, []);
return {
...node,
width: refNode.width,
height: refNode.height,
data: {
...node.data,
resizedByUser: true,
},
};
});
},
null,
refElementId
);
},
[resolveNodeOverlap]
);

return {
distributeGapVertically,
Expand Down
Loading

0 comments on commit e6d5bfd

Please sign in to comment.