Skip to content

Commit

Permalink
[3397] Add helper lines when resizing nodes
Browse files Browse the repository at this point in the history
Bug: #3397
Signed-off-by: Florian ROUËNÉ <florian.rouene@obeosoft.com>
  • Loading branch information
frouene committed Apr 18, 2024
1 parent 14f6efe commit b2f5996
Show file tree
Hide file tree
Showing 3 changed files with 239 additions and 40 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ They still support returning an `java.time.Instant` object directly.
- https://github.com/eclipse-sirius/sirius-web/issues/3376[#3376] [diagram] Prevent `getToolSection` to be called during multi-selection
- https://github.com/eclipse-sirius/sirius-web/issues/3377[#3377] [diagram] Only synchronize selection with explorer at the end of the process
- https://github.com/eclipse-sirius/sirius-web/issues/3359[#3359] Add report on document upload

- https://github.com/eclipse-sirius/sirius-web/issues/3397[#3397] [diagram] Add helper lines when resizing nodes

== v2024.3.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ export const DiagramRenderer = ({ diagramRefreshedEventPayload }: DiagramRendere
});
}
},
[setNodes, targetNodeId, draggedNode?.id]
[setNodes, targetNodeId, draggedNode?.id, helperLinesEnabled]
);

const handleEdgesChange: OnEdgesChange = useCallback((changes: EdgeChange[]) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,31 +10,34 @@
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
import { useState } from 'react';
import { Node, NodeChange, NodePositionChange, useReactFlow } from 'reactflow';
import { useCallback, useState } from 'react';
import { Node, NodeChange, NodeDimensionChange, NodePositionChange, useReactFlow } from 'reactflow';
import { UseHelperLinesState, UseHelperLinesValue, HelperLines } from './useHelperLines.types';
import { NodeData, EdgeData } from '../DiagramRenderer.types';
import { isDescendantOf } from '../layout/layoutNode';

const isMove = (change: NodeChange): change is NodePositionChange =>
change.type === 'position' && typeof change.dragging === 'boolean' && change.dragging;
const isMove = (change: NodeChange, dragging: boolean): change is NodePositionChange =>
change.type === 'position' && (!dragging || (typeof change.dragging === 'boolean' && change.dragging));

const getHelperLines = (
const isResize = (change: NodeChange): change is NodeDimensionChange =>
change.type === 'dimensions' && (change.resizing ?? false);

const getHelperLinesForMove = (
change: NodePositionChange,
movingNode: Node<NodeData>,
nodes: Node<NodeData>[]
): HelperLines => {
const noHelperLines: HelperLines = { horizontal: null, vertical: null, snapX: 0, snapY: 0 };
const getNodeById = (id: string | undefined) => nodes.find((n) => n.id === id);
if (change.positionAbsolute) {
const movingNodeBounds = {
const getNodeById = (id: string | undefined) => nodes.find((n) => n.id === id);
let verticalSnapGap: number = 10;
let horizontalSnapGap: number = 10;
const movingNodeBounds: { x1: number; x2: number; y1: number; y2: number } = {
x1: change.positionAbsolute.x,
x2: change.positionAbsolute.x + (movingNode.width ?? 0),
y1: change.positionAbsolute.y,
y2: change.positionAbsolute.y + (movingNode.height ?? 0),
};
let verticalSnapGap: number = 10;
let horizontalSnapGap: number = 10;
return nodes
.filter((node) => node.id != movingNode.id)
.filter((node) => !isDescendantOf(movingNode, node, getNodeById))
Expand Down Expand Up @@ -125,44 +128,240 @@ const getHelperLines = (
return noHelperLines;
};

const getHelperLinesForResize = (
change: NodeDimensionChange,
resizingNode: Node<NodeData>,
nodes: Node<NodeData>[]
): HelperLines => {
const noHelperLines: HelperLines = { horizontal: null, vertical: null, snapX: 0, snapY: 0 };
if (resizingNode.positionAbsolute && change.dimensions) {
const getNodeById = (id: string | undefined) => nodes.find((n) => n.id === id);
let verticalSnapGap: number = 10;
let horizontalSnapGap: number = 10;
const resizingNodeBounds: { x1: number; x2: number; y1: number; y2: number } = {
x1: resizingNode.positionAbsolute.x,
x2: resizingNode.positionAbsolute.x + (change.dimensions.width ?? 0),
y1: resizingNode.positionAbsolute.y,
y2: resizingNode.positionAbsolute.y + (change.dimensions.height ?? 0),
};
return nodes
.filter((node) => node.id != resizingNode.id)
.filter((node) => !isDescendantOf(resizingNode, node, getNodeById))
.reduce<HelperLines>((helperLines, otherNode) => {
if (otherNode.positionAbsolute) {
const otherNodeBounds = {
x1: otherNode.positionAbsolute.x,
x2: otherNode.positionAbsolute.x + (otherNode.width ?? 0),
y1: otherNode.positionAbsolute.y,
y2: otherNode.positionAbsolute.y + (otherNode.height ?? 0),
};

const x2x1Gap = Math.abs(resizingNodeBounds.x2 - otherNodeBounds.x1);
if (x2x1Gap < verticalSnapGap) {
helperLines.vertical = otherNodeBounds.x1;
helperLines.snapX = otherNodeBounds.x1;
verticalSnapGap = x2x1Gap;
}

const x2x2Gap = Math.abs(resizingNodeBounds.x2 - otherNodeBounds.x2);
if (x2x2Gap < verticalSnapGap) {
helperLines.vertical = otherNodeBounds.x2;
helperLines.snapX = otherNodeBounds.x2;
verticalSnapGap = x2x2Gap;
}

const y2y1Gap = Math.abs(resizingNodeBounds.y2 - otherNodeBounds.y1);
if (y2y1Gap < horizontalSnapGap) {
helperLines.horizontal = otherNodeBounds.y1;
helperLines.snapY = otherNodeBounds.y1;
horizontalSnapGap = y2y1Gap;
}

const y2y2Gap = Math.abs(resizingNodeBounds.y2 - otherNodeBounds.y2);
if (y2y2Gap < horizontalSnapGap) {
helperLines.horizontal = otherNodeBounds.y2;
helperLines.snapY = otherNodeBounds.y2;
horizontalSnapGap = y2y2Gap;
}
}
return helperLines;
}, noHelperLines);
}
return noHelperLines;
};

const getHelperLinesForResizeAndMove = (
resizingChange: NodeDimensionChange,
movingChange: NodePositionChange,
resizingNode: Node<NodeData>,
nodes: Node<NodeData>[]
): HelperLines => {
const noHelperLines: HelperLines = { horizontal: null, vertical: null, snapX: 0, snapY: 0 };
if (resizingNode.positionAbsolute && resizingChange.dimensions && movingChange.position) {
const getNodeById = (id: string | undefined) => nodes.find((n) => n.id === id);
let verticalSnapGap: number = 10;
let horizontalSnapGap: number = 10;
const nodeBounds: { x1: number; x2: number; y1: number; y2: number } = {
x1: movingChange.position.x + resizingNode.positionAbsolute.x - resizingNode.position.x,
x2:
movingChange.position.x +
resizingNode.positionAbsolute.x -
resizingNode.position.x +
(resizingChange.dimensions.width ?? 0),
y1: movingChange.position.y + resizingNode.positionAbsolute.y - resizingNode.position.y,
y2:
movingChange.position.y +
resizingNode.positionAbsolute.y -
resizingNode.position.y +
(resizingChange.dimensions.height ?? 0),
};
return nodes
.filter((node) => node.id != resizingNode.id)
.filter((node) => !isDescendantOf(resizingNode, node, getNodeById))
.reduce<HelperLines>((helperLines, otherNode) => {
if (otherNode.positionAbsolute) {
const otherNodeBounds = {
x1: otherNode.positionAbsolute.x,
x2: otherNode.positionAbsolute.x + (otherNode.width ?? 0),
y1: otherNode.positionAbsolute.y,
y2: otherNode.positionAbsolute.y + (otherNode.height ?? 0),
};

const x1x1Gap = Math.abs(nodeBounds.x1 - otherNodeBounds.x1);
if (x1x1Gap < verticalSnapGap) {
helperLines.vertical = otherNodeBounds.x1;
helperLines.snapX = otherNodeBounds.x1;
verticalSnapGap = x1x1Gap;
}

const x1x2Gap = Math.abs(nodeBounds.x1 - otherNodeBounds.x2);
if (x1x2Gap < verticalSnapGap) {
helperLines.vertical = otherNodeBounds.x2;
helperLines.snapX = otherNodeBounds.x2;
verticalSnapGap = x1x2Gap;
}

const y1y1Gap = Math.abs(nodeBounds.y1 - otherNodeBounds.y1);
if (y1y1Gap < horizontalSnapGap) {
helperLines.horizontal = otherNodeBounds.y1;
helperLines.snapY = otherNodeBounds.y1;
horizontalSnapGap = y1y1Gap;
}

const y1y2Gap = Math.abs(nodeBounds.y1 - otherNodeBounds.y2);
if (y1y2Gap < horizontalSnapGap) {
helperLines.horizontal = otherNodeBounds.y2;
helperLines.snapY = otherNodeBounds.y2;
horizontalSnapGap = y1y2Gap;
}
}
return helperLines;
}, noHelperLines);
}
return noHelperLines;
};

export const useHelperLines = (): UseHelperLinesValue => {
const [enabled, setEnabled] = useState<boolean>(false);
const [state, setState] = useState<UseHelperLinesState>({ vertical: null, horizontal: null });
const { getNodes } = useReactFlow<NodeData, EdgeData>();

const applyHelperLines = (changes: NodeChange[]): NodeChange[] => {
if (enabled && changes.length === 1 && changes[0]) {
const change = changes[0];
if (isMove(change)) {
const movingNode = getNodes().find((node) => node.id === change.id);
if (movingNode && !movingNode.data.pinned) {
const helperLines: HelperLines = getHelperLines(change, movingNode, getNodes());
setState({ vertical: helperLines.vertical, horizontal: helperLines.horizontal });
let snapOffsetX: number = 0;
let snapOffsetY: number = 0;
let parentNode = getNodes().find((node) => node.id === movingNode.parentNode);
while (parentNode) {
snapOffsetX -= parentNode.position.x;
snapOffsetY -= parentNode.position.y;
parentNode = getNodes().find((node) => node.id === parentNode?.parentNode ?? '');
}
if (helperLines.snapX && change.position) {
change.position.x = helperLines.snapX + snapOffsetX;
}
if (helperLines.snapY && change.position) {
change.position.y = helperLines.snapY + snapOffsetY;
const applyHelperLines = useCallback(
(changes: NodeChange[]): NodeChange[] => {
if (enabled && changes.length === 1 && changes[0]) {
const change = changes[0];
if (isMove(change, true)) {
const movingNode = getNodes().find((node) => node.id === change.id);
if (movingNode && !movingNode.data.pinned) {
const helperLines: HelperLines = getHelperLinesForMove(change, movingNode, getNodes());
setState({ vertical: helperLines.vertical, horizontal: helperLines.horizontal });
let snapOffsetX: number = 0;
let snapOffsetY: number = 0;
let parentNode = getNodes().find((node) => node.id === movingNode.parentNode);
while (parentNode) {
snapOffsetX -= parentNode.position.x;
snapOffsetY -= parentNode.position.y;
parentNode = getNodes().find((node) => node.id === parentNode?.parentNode ?? '');
}
if (helperLines.snapX && change.position) {
change.position.x = helperLines.snapX + snapOffsetX;
}
if (helperLines.snapY && change.position) {
change.position.y = helperLines.snapY + snapOffsetY;
}
}
} else if (isResize(change)) {
const resizingNode = getNodes().find((node) => node.id === change.id);
if (resizingNode) {
const helperLines: HelperLines = getHelperLinesForResize(change, resizingNode, getNodes());
setState({ vertical: helperLines.vertical, horizontal: helperLines.horizontal });
if (helperLines.snapX && change.dimensions && resizingNode.positionAbsolute) {
change.dimensions.width = Math.abs(resizingNode.positionAbsolute.x - helperLines.snapX);
}
if (helperLines.snapY && change.dimensions && resizingNode.positionAbsolute) {
change.dimensions.height = Math.abs(resizingNode.positionAbsolute.y - helperLines.snapY);
}
}
}
} else if (enabled && changes.length === 2 && changes[0] && changes[1]) {
const movingChange = changes[0];
const resizingChange = changes[1];
if (isMove(movingChange, false) && isResize(resizingChange)) {
const resizingNode = getNodes().find((node) => node.id === movingChange.id);
if (resizingNode) {
const helperLines: HelperLines = getHelperLinesForResizeAndMove(
resizingChange,
movingChange,
resizingNode,
getNodes()
);
setState({ vertical: helperLines.vertical, horizontal: helperLines.horizontal });
let snapOffsetX: number = 0;
let snapOffsetY: number = 0;
let parentNode = getNodes().find((node) => node.id === resizingNode.parentNode);
while (parentNode) {
snapOffsetX -= parentNode.position.x;
snapOffsetY -= parentNode.position.y;
parentNode = getNodes().find((node) => node.id === parentNode?.parentNode ?? '');
}
if (
helperLines.snapX &&
resizingChange.dimensions &&
movingChange.position &&
resizingNode.positionAbsolute &&
resizingNode.width
) {
movingChange.position.x = helperLines.snapX + snapOffsetX;
resizingChange.dimensions.width =
resizingNode.width + resizingNode.positionAbsolute.x - helperLines.snapX;
}
if (
helperLines.snapY &&
resizingChange.dimensions &&
movingChange.position &&
resizingNode.positionAbsolute &&
resizingNode.height
) {
movingChange.position.y = helperLines.snapY + snapOffsetY;
resizingChange.dimensions.height =
resizingNode.height + resizingNode.positionAbsolute.y - helperLines.snapY;
}
}
}
}
}
return changes;
};
return changes;
},
[enabled]
);

const resetHelperLines = (changes: NodeChange[]): void => {
if (enabled && changes[0]?.type === 'reset') {
setState({ vertical: null, horizontal: null });
}
};
const resetHelperLines = useCallback(
(changes: NodeChange[]): void => {
if (enabled && changes[0]?.type === 'reset') {
setState({ vertical: null, horizontal: null });
}
},
[enabled]
);

return {
helperLinesEnabled: enabled,
Expand Down

0 comments on commit b2f5996

Please sign in to comment.