diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index e2905e4164..f67cc0e529 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -155,6 +155,7 @@ They still support returning an `java.time.Instant` object directly. - https://github.com/eclipse-sirius/sirius-web/issues/3391[#3391] [diagram] Accept gradient for node background - https://github.com/eclipse-sirius/sirius-web/issues/3435[#3435] [diagram] Extract diagram style from useDropNode - https://github.com/eclipse-sirius/sirius-web/issues/3453[#3453] [diagram] Memoizing edges and nodes style +- https://github.com/eclipse-sirius/sirius-web/issues/3450[#3450] [diagram] Add `calculateCustomNodeBorderNodePosition` method to position border nodes according to the real layout of the custom node == v2024.3.0 diff --git a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/border/useBorderChange.tsx b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/border/useBorderChange.tsx index d963eb38d3..f7c23402c5 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/border/useBorderChange.tsx +++ b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/border/useBorderChange.tsx @@ -10,12 +10,15 @@ * Contributors: * Obeo - initial API and implementation *******************************************************************************/ -import { useCallback } from 'react'; +import { useCallback, useContext } from 'react'; import { Node, NodeChange, useReactFlow, XYPosition } from 'reactflow'; import { EdgeData, NodeData, BorderNodePosition } from '../DiagramRenderer.types'; import { findBorderNodePosition } from '../layout/layoutBorderNodes'; import { borderNodeOffset } from '../layout/layoutParams'; +import { DiagramNodeType } from '../node/NodeTypes.types'; import { UseBorderChangeValue } from './useBorderChange.types'; +import { NodeTypeContextValue } from '../../contexts/NodeContext.types'; +import { NodeTypeContext } from '../../contexts/NodeContext'; const isNewPositionInsideIsParent = (newNodePosition: XYPosition, movedNode: Node, parentNode: Node): boolean => { if (movedNode.width && movedNode.height && parentNode?.positionAbsolute && parentNode.width && parentNode.height) { @@ -55,6 +58,7 @@ const findNearestBorderPosition = ( export const useBorderChange = (): UseBorderChangeValue => { const { getNodes } = useReactFlow(); + const { nodeLayoutHandlers } = useContext(NodeTypeContext); const transformBorderNodeChanges = useCallback((changes: NodeChange[], oldNodes: Node[]): NodeChange[] => { return changes.map((change) => { @@ -62,10 +66,14 @@ export const useBorderChange = (): UseBorderChangeValue => { const movedNode = getNodes().find((node) => change.id === node.id); if (movedNode && movedNode.data.isBorderNode) { const parentNode = getNodes().find((node) => movedNode.parentNode === node.id); + const parentLayoutHandler = nodeLayoutHandlers.find((nodeLayoutHandler) => + nodeLayoutHandler.canHandle(parentNode as Node) + ); if ( parentNode && parentNode.positionAbsolute && - isNewPositionInsideIsParent(change.positionAbsolute, movedNode, parentNode) + isNewPositionInsideIsParent(change.positionAbsolute, movedNode, parentNode) && + !parentLayoutHandler?.calculateCustomNodeBorderNodePosition ) { const nearestBorder = findNearestBorderPosition(change.positionAbsolute, parentNode); if (nearestBorder === BorderNodePosition.NORTH) { @@ -91,6 +99,18 @@ export const useBorderChange = (): UseBorderChangeValue => { if (oldMovedNode && oldMovedNode.data.borderNodePosition !== newPosition) { oldMovedNode.data.borderNodePosition = newPosition; } + if (parentLayoutHandler?.calculateCustomNodeBorderNodePosition && parentNode) { + change.position = parentLayoutHandler.calculateCustomNodeBorderNodePosition( + parentNode, + { + x: change.position.x, + y: change.position.y, + width: movedNode.width ?? 0, + height: movedNode.height ?? 0, + }, + true + ); + } } } return change; diff --git a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/layout/LayoutEngine.types.ts b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/layout/LayoutEngine.types.ts index d30a7c1d64..9cb7759bde 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/layout/LayoutEngine.types.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/layout/LayoutEngine.types.ts @@ -11,7 +11,7 @@ * Obeo - initial API and implementation *******************************************************************************/ -import { HandleElement, Node, Position, XYPosition } from 'reactflow'; +import { Dimensions, HandleElement, Node, Position, XYPosition } from 'reactflow'; import { NodeData } from '../DiagramRenderer.types'; import { DiagramNodeType } from '../node/NodeTypes.types'; import { RawDiagram, ForcedDimensions } from './layout.types'; @@ -46,4 +46,10 @@ export interface INodeLayoutHandler { handlePosition: Position, handle: HandleElement ): XYPosition; + + calculateCustomNodeBorderNodePosition?( + node: Node, + borderNode: XYPosition & Dimensions, + isDragging: boolean + ): XYPosition; } diff --git a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/layout/layoutNode.ts b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/layout/layoutNode.ts index b9bcdc9d0f..dd00697a10 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/layout/layoutNode.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/layout/layoutNode.ts @@ -10,7 +10,7 @@ * Contributors: * Obeo - initial API and implementation *******************************************************************************/ -import { Box, Node, Rect, XYPosition, boxToRect, rectToBox } from 'reactflow'; +import { Box, Dimensions, Node, Rect, XYPosition, boxToRect, rectToBox } from 'reactflow'; import { NodeData, InsideLabel } from '../DiagramRenderer.types'; import { RawDiagram } from './layout.types'; import { @@ -247,101 +247,122 @@ const getRightMostSibling = ( export const setBorderNodesPosition = ( borderNodes: Node[], nodeToLayout: Node, - previousDiagram: RawDiagram | null + previousDiagram: RawDiagram | null, + calculateCustomNodeBorderNodePosition?: + | ((node: Node, borderNode: XYPosition & Dimensions, isDragging: boolean) => XYPosition) + | undefined ): void => { - const borderNodesEast = borderNodes.filter(isEastBorderNode); - borderNodesEast.forEach((child) => { - const previousBorderNode = (previousDiagram?.nodes ?? []).find((previousNode) => previousNode.id === child.id); - const previousPosition = computePreviousPosition(previousBorderNode, child); - if (previousPosition) { - let newY = previousPosition.y; - if (nodeToLayout.height && newY > nodeToLayout.height) { - newY = nodeToLayout.height - borderNodeOffset; - } - child.position = { - x: nodeToLayout.width ?? 0, - y: newY, - }; - } else { - child.position = { x: nodeToLayout.width ?? 0, y: defaultNodeMargin }; - const previousSibling = getLowestSibling(borderNodesEast, previousDiagram); - if (previousSibling) { - child.position = { ...child.position, y: previousSibling.position.y + (previousSibling.height ?? 0) + gap }; - child.extent = getBorderNodeExtent(nodeToLayout, child); + if (calculateCustomNodeBorderNodePosition) { + borderNodes.forEach((child) => { + const previousBorderNode = (previousDiagram?.nodes ?? []).find((previousNode) => previousNode.id === child.id); + const previousPosition = computePreviousPosition(previousBorderNode, child); + if (previousPosition) { + child.position = calculateCustomNodeBorderNodePosition( + nodeToLayout, + { + ...previousPosition, + width: child.width ?? 0, + height: child.height ?? 0, + }, + false + ); } - } - child.position.x = child.position.x - borderNodeOffset; - }); - - const borderNodesWest = borderNodes.filter(isWestBorderNode); - borderNodesWest.forEach((child) => { - const previousBorderNode = (previousDiagram?.nodes ?? []).find((previousNode) => previousNode.id === child.id); - const previousPosition = computePreviousPosition(previousBorderNode, child); - if (previousPosition) { - let newY = previousPosition.y; - if (nodeToLayout.height && newY > nodeToLayout.height) { - newY = nodeToLayout.height - borderNodeOffset; - } - child.position = { - x: 0 - (child.width ?? 0), - y: newY, - }; - } else { - child.position = { x: 0 - (child.width ?? 0), y: defaultNodeMargin }; - const previousSibling = getLowestSibling(borderNodesWest, previousDiagram); - if (previousSibling) { - child.position = { ...child.position, y: previousSibling.position.y + (previousSibling.height ?? 0) + gap }; - } - } - child.position.x = child.position.x + borderNodeOffset; - }); - - const borderNodesSouth = borderNodes.filter(isSouthBorderNode); - borderNodesSouth.forEach((child) => { - const previousBorderNode = (previousDiagram?.nodes ?? []).find((previousNode) => previousNode.id === child.id); - const previousPosition = computePreviousPosition(previousBorderNode, child); - if (previousPosition) { - let newX = previousPosition.x; - if (nodeToLayout.width && newX > nodeToLayout.width) { - newX = nodeToLayout.width - borderNodeOffset; + }); + } else { + const borderNodesEast = borderNodes.filter(isEastBorderNode); + borderNodesEast.forEach((child) => { + const previousBorderNode = (previousDiagram?.nodes ?? []).find((previousNode) => previousNode.id === child.id); + const previousPosition = computePreviousPosition(previousBorderNode, child); + if (previousPosition) { + let newY = previousPosition.y; + if (nodeToLayout.height && newY > nodeToLayout.height) { + newY = nodeToLayout.height - borderNodeOffset; + } + child.position = { + x: nodeToLayout.width ?? 0, + y: newY, + }; + } else { + child.position = { x: nodeToLayout.width ?? 0, y: defaultNodeMargin }; + const previousSibling = getLowestSibling(borderNodesEast, previousDiagram); + if (previousSibling) { + child.position = { ...child.position, y: previousSibling.position.y + (previousSibling.height ?? 0) + gap }; + child.extent = getBorderNodeExtent(nodeToLayout, child); + } } - child.position = { - x: newX, - y: nodeToLayout.height ?? 0, - }; - } else { - child.position = { x: defaultNodeMargin, y: nodeToLayout.height ?? 0 }; - const previousSibling = getRightMostSibling(borderNodesSouth, previousDiagram); - if (previousSibling) { - child.position = { ...child.position, x: previousSibling.position.x + (previousSibling.width ?? 0) + gap }; - child.extent = getBorderNodeExtent(nodeToLayout, child); + child.position.x = child.position.x - borderNodeOffset; + }); + + const borderNodesWest = borderNodes.filter(isWestBorderNode); + borderNodesWest.forEach((child) => { + const previousBorderNode = (previousDiagram?.nodes ?? []).find((previousNode) => previousNode.id === child.id); + const previousPosition = computePreviousPosition(previousBorderNode, child); + if (previousPosition) { + let newY = previousPosition.y; + if (nodeToLayout.height && newY > nodeToLayout.height) { + newY = nodeToLayout.height - borderNodeOffset; + } + child.position = { + x: 0 - (child.width ?? 0), + y: newY, + }; + } else { + child.position = { x: 0 - (child.width ?? 0), y: defaultNodeMargin }; + const previousSibling = getLowestSibling(borderNodesWest, previousDiagram); + if (previousSibling) { + child.position = { ...child.position, y: previousSibling.position.y + (previousSibling.height ?? 0) + gap }; + } } - } - child.position.y = child.position.y - borderNodeOffset; - }); - - const borderNodesNorth = borderNodes.filter(isNorthBorderNode); - borderNodesNorth.forEach((child) => { - const previousBorderNode = (previousDiagram?.nodes ?? []).find((previousNode) => previousNode.id === child.id); - const previousPosition = computePreviousPosition(previousBorderNode, child); - if (previousPosition) { - let newX = previousPosition.x; - if (nodeToLayout.width && newX > nodeToLayout.width) { - newX = nodeToLayout.width - borderNodeOffset; + child.position.x = child.position.x + borderNodeOffset; + }); + + const borderNodesSouth = borderNodes.filter(isSouthBorderNode); + borderNodesSouth.forEach((child) => { + const previousBorderNode = (previousDiagram?.nodes ?? []).find((previousNode) => previousNode.id === child.id); + const previousPosition = computePreviousPosition(previousBorderNode, child); + if (previousPosition) { + let newX = previousPosition.x; + if (nodeToLayout.width && newX > nodeToLayout.width) { + newX = nodeToLayout.width - borderNodeOffset; + } + child.position = { + x: newX, + y: nodeToLayout.height ?? 0, + }; + } else { + child.position = { x: defaultNodeMargin, y: nodeToLayout.height ?? 0 }; + const previousSibling = getRightMostSibling(borderNodesSouth, previousDiagram); + if (previousSibling) { + child.position = { ...child.position, x: previousSibling.position.x + (previousSibling.width ?? 0) + gap }; + child.extent = getBorderNodeExtent(nodeToLayout, child); + } } - child.position = { - x: newX, - y: 0 - (child.height ?? 0), - }; - } else { - child.position = { x: defaultNodeMargin, y: 0 - (child.height ?? 0) }; - const previousSibling = getRightMostSibling(borderNodesNorth, previousDiagram); - if (previousSibling) { - child.position = { ...child.position, x: previousSibling.position.x + (previousSibling.width ?? 0) + gap }; + child.position.y = child.position.y - borderNodeOffset; + }); + + const borderNodesNorth = borderNodes.filter(isNorthBorderNode); + borderNodesNorth.forEach((child) => { + const previousBorderNode = (previousDiagram?.nodes ?? []).find((previousNode) => previousNode.id === child.id); + const previousPosition = computePreviousPosition(previousBorderNode, child); + if (previousPosition) { + let newX = previousPosition.x; + if (nodeToLayout.width && newX > nodeToLayout.width) { + newX = nodeToLayout.width - borderNodeOffset; + } + child.position = { + x: newX, + y: 0 - (child.height ?? 0), + }; + } else { + child.position = { x: defaultNodeMargin, y: 0 - (child.height ?? 0) }; + const previousSibling = getRightMostSibling(borderNodesNorth, previousDiagram); + if (previousSibling) { + child.position = { ...child.position, x: previousSibling.position.x + (previousSibling.width ?? 0) + gap }; + } } - } - child.position.y = child.position.y + borderNodeOffset; - }); + child.position.y = child.position.y + borderNodeOffset; + }); + } }; /** diff --git a/packages/sirius-web/frontend/sirius-web/src/nodes/EllipseNodeLayoutHandler.ts b/packages/sirius-web/frontend/sirius-web/src/nodes/EllipseNodeLayoutHandler.ts index ae6b0c0dda..edfc43d4fe 100644 --- a/packages/sirius-web/frontend/sirius-web/src/nodes/EllipseNodeLayoutHandler.ts +++ b/packages/sirius-web/frontend/sirius-web/src/nodes/EllipseNodeLayoutHandler.ts @@ -22,7 +22,6 @@ import { computePreviousPosition, computePreviousSize, findNodeIndex, - getBorderNodeExtent, getChildNodePosition, getEastBorderNodeFootprintHeight, getHeaderHeightFootprint, @@ -33,8 +32,22 @@ import { getSouthBorderNodeFootprintWidth, getWestBorderNodeFootprintHeight, setBorderNodesPosition, + getBorderNodeExtent, } from '@eclipse-sirius/sirius-components-diagrams'; -import { HandleElement, Node, Position, XYPosition } from 'reactflow'; +import { Dimensions, HandleElement, Node, Position, XYPosition } from 'reactflow'; + +const borderNodeOffset = 5; + +const findBorderNodePosition = (borderNodePosition: XYPosition | undefined, parentNode: Node | undefined): number => { + if (borderNodePosition && parentNode?.width && parentNode.height) { + if (borderNodePosition.y < parentNode.height / 2) { + return borderNodePosition.x < parentNode.width / 2 ? 0 : 1; + } else { + return borderNodePosition.x < parentNode.width / 2 ? 2 : 3; + } + } + return null; +}; export class EllipseNodeLayoutHandler implements INodeLayoutHandler { canHandle(node: Node) { @@ -150,7 +163,7 @@ export class EllipseNodeLayoutHandler implements INodeLayoutHandler { borderNodes.forEach((borderNode) => { borderNode.extent = getBorderNodeExtent(node, borderNode); }); - setBorderNodesPosition(borderNodes, node, previousDiagram); + setBorderNodesPosition(borderNodes, node, previousDiagram, this.calculateCustomNodeBorderNodePosition); } calculateCustomNodeEdgeHandlePosition( @@ -191,4 +204,73 @@ export class EllipseNodeLayoutHandler implements INodeLayoutHandler { y: realY + offsetY, }; } + + calculateCustomNodeBorderNodePosition( + parentNode: Node, + borderNode: XYPosition & Dimensions, + isDragging: boolean + ): XYPosition { + let offsetX: number = 0; + let offsetY: number = 0; + const parentNodeWidth: number = parentNode.width ?? 0; + const parentNodeHeight: number = parentNode.height ?? 0; + const a: number = parentNodeWidth / 2; + const b: number = parentNodeHeight / 2; + const pos: number = findBorderNodePosition(borderNode, parentNode); + let realY: number = borderNode.y; + let realX: number; + if (borderNode.x < 0) { + return { + x: -borderNode.width + borderNodeOffset, + y: b - borderNode.height / 2, + }; + } else if (borderNode.x >= parentNodeWidth - borderNodeOffset) { + return { + x: parentNodeWidth - borderNodeOffset, + y: b - borderNode.height / 2, + }; + } else { + realX = borderNode.x; + } + if (!isDragging) { + switch (pos) { + case 0: + case 2: + realX += borderNode.width; + break; + default: + break; + } + } + switch (pos) { + case 0: + realY = Math.sqrt((1 - Math.pow(realX - a, 2) / Math.pow(a, 2)) * Math.pow(b, 2)) + b; + realY = parentNodeHeight - realY; + offsetY = -borderNode.height + borderNodeOffset; + offsetX = -borderNode.width; + break; + case 1: + realY = Math.sqrt((1 - Math.pow(realX - a, 2) / Math.pow(a, 2)) * Math.pow(b, 2)) + b; + realY = parentNodeHeight - realY; + offsetY = -borderNode.height + borderNodeOffset; + break; + case 2: + realY = Math.sqrt((1 - Math.pow(realX - a, 2) / Math.pow(a, 2)) * Math.pow(b, 2)) + b; + offsetY = -borderNodeOffset; + offsetX = -borderNode.width; + break; + case 3: + realY = Math.sqrt((1 - Math.pow(realX - a, 2) / Math.pow(a, 2)) * Math.pow(b, 2)) + b; + offsetY = -borderNodeOffset; + break; + } + if (isNaN(realY)) { + realY = b; + } + + return { + x: realX + offsetX, + y: realY + offsetY, + }; + } }