From b8ead136d5486e49d7458aeb3d9b596373e23ed4 Mon Sep 17 00:00:00 2001 From: Michael Charfadi Date: Tue, 30 Apr 2024 13:05:39 +0200 Subject: [PATCH] [fix] Revert 3392 Prevents edge from passing through another node MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michaƫl Charfadi --- CHANGELOG.adoc | 2 - .../cypress/e2e/project/diagrams/edges.cy.ts | 63 ++--- package-lock.json | 59 +---- .../sirius-components-diagrams/package.json | 1 - .../src/converter/convertDiagram.ts | 4 +- .../src/renderer/DiagramRenderer.types.ts | 4 +- .../src/renderer/edge/EdgeLayout.ts | 6 +- .../src/renderer/edge/EdgeTypes.ts | 4 +- .../src/renderer/edge/EdgeWrapper.tsx | 248 ------------------ .../src/renderer/edge/EdgeWrapper.types.ts | 19 -- .../src/renderer/edge/MultiLabelEdge.tsx | 104 ++++++-- .../src/renderer/edge/MultiLabelEdge.types.ts | 14 +- .../renderer/handles/ConnectionHandles.tsx | 43 ++- .../src/renderer/layout/layoutNode.ts | 16 +- .../src/renderer/layout/useArrangeAll.ts | 7 +- .../frontend/sirius-web/package.json | 1 - 16 files changed, 175 insertions(+), 420 deletions(-) delete mode 100644 packages/diagrams/frontend/sirius-components-diagrams/src/renderer/edge/EdgeWrapper.tsx delete mode 100644 packages/diagrams/frontend/sirius-components-diagrams/src/renderer/edge/EdgeWrapper.types.ts diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 437cb49145..0802533e4a 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -67,7 +67,6 @@ Accordingly, `IObjectSearchService` and `IIdentityService` now handle actual `IE - Move to docker compose v2 for our GitHub Actions workflow. - [test] Switch to ArchUnit 1.3.0 to restore architectural tests which were failing recently - https://github.com/eclipse-sirius/sirius-web/issues/3277[#3277] [gantt] Move to @ObeoNetwork/gantt-task-react 0.4.9 to benefit for enhancements -- https://github.com/eclipse-sirius/sirius-web/issues/3392[#3392] [diagrams] Add dependency to @tisoap/react-flow-smart-edge 3.0.0 === Bug fixes @@ -139,7 +138,6 @@ They still support returning an `java.time.Instant` object directly. - https://github.com/eclipse-sirius/sirius-web/issues/3279[#3279] [core] Image URLs are now sanitized in a similar manner in all datafetchers, as a result absolute URLs to external images are better supported and a leading slash is now required anymore. - https://github.com/eclipse-sirius/sirius-web/issues/3296[#3296] [diagram] Add a hook `useStore` to retrieve local `setNodes`, `setEdges`, `onEdgesChange` and `onNodesChange`, this hook is used to align how nodes and edges are manipulated during processing of ReactFlow events. - https://github.com/eclipse-sirius/sirius-web/issues/3397[#3397] [diagram] Add helper lines when resizing nodes -- https://github.com/eclipse-sirius/sirius-web/issues/3392[#3392] [diagram] Prevent edge from passing through another node == v2024.3.0 diff --git a/integration-tests/cypress/e2e/project/diagrams/edges.cy.ts b/integration-tests/cypress/e2e/project/diagrams/edges.cy.ts index 3639893c4f..08f14fc7d0 100644 --- a/integration-tests/cypress/e2e/project/diagrams/edges.cy.ts +++ b/integration-tests/cypress/e2e/project/diagrams/edges.cy.ts @@ -22,37 +22,37 @@ describe('Diagram - edges', () => { let domainName: string = ''; before(() => - new Studio().createStudioProject().then((createdProjectData) => { - studioProjectId = createdProjectData.projectId; - new Project().visit(createdProjectData.projectId); - const explorer = new Explorer(); - explorer.expand('DomainNewModel'); - cy.get('[title="domain::Domain"]').then(($div) => { - domainName = $div.data().testid; - explorer.expand(`${domainName}`); - explorer.createObject('Entity1', 'Relation'); - const details = new Details(); - details.getCheckBox('Containment').check(); - details.openReferenceWidgetOptions('Target Type'); - details.selectReferenceWidgetOption('Entity2'); + new Studio().createStudioProject().then((createdProjectData) => { + studioProjectId = createdProjectData.projectId; + new Project().visit(createdProjectData.projectId); + const explorer = new Explorer(); + explorer.expand('DomainNewModel'); + cy.get('[title="domain::Domain"]').then(($div) => { + domainName = $div.data().testid; + explorer.expand(`${domainName}`); + explorer.createObject('Entity1', 'Relation'); + const details = new Details(); + details.getCheckBox('Containment').check(); + details.openReferenceWidgetOptions('Target Type'); + details.selectReferenceWidgetOption('Entity2'); - explorer.expand('ViewNewModel'); - explorer.expand('View'); - explorer.expand(`${domainName} Diagram Description`); - explorer.expand('Entity1 Node'); - details.openReferenceWidgetOptions('Reused Child Node Descriptions'); - details.selectReferenceWidgetOption('Entity2 Node'); - details.getTextField('Default Width Expression').type('290{enter}'); - details.getTextField('Default Height Expression').type('290{enter}'); - }); - }) + explorer.expand('ViewNewModel'); + explorer.expand('View'); + explorer.expand(`${domainName} Diagram Description`); + explorer.expand('Entity1 Node'); + details.openReferenceWidgetOptions('Reused Child Node Descriptions'); + details.selectReferenceWidgetOption('Entity2 Node'); + details.getTextField('Default Width Expression').type('300{enter}'); + details.getTextField('Default Height Expression').type('300{enter}'); + }); + }) ); after(() => cy.deleteProject(studioProjectId)); context('When we create a new instance project', () => { let instanceProjectId: string = ''; - beforeEach(() => { + beforeEach(()=> { const studio = new Studio(); studio.createProjectFromDomain('Cypress - Studio Instance', domainName, 'Root').then((res) => { instanceProjectId = res.projectId; @@ -77,16 +77,11 @@ describe('Diagram - edges', () => { details.openReferenceWidgetOptions('Linked To'); details.selectReferenceWidgetOption('Entity2'); diagram.fitToScreen(); - diagram.getEdgePaths('diagram').should('have.length', 1); - diagram - .getEdgePaths('diagram') - .eq(0) - .invoke('attr', 'd') - .then((dValue) => { - expect(diagram.roundSvgPathData(dValue ?? '')).to.equal( - 'M140.00L140.00L140.00L120.00L100.00L80.00L80.00L80.00' - ); - }); + diagram.getEdgePaths('diagram').should('have.length',1); + diagram.getEdgePaths('diagram').eq(0).invoke('attr', 'd') + .then((dValue) => { + expect(diagram.roundSvgPathData(dValue?? '')).to.equal('M150.00L150.00Q150.00L88.00Q83.00L83.00L83.00'); + }); }); }); }); diff --git a/package-lock.json b/package-lock.json index 58f7a1795a..0a82a44a11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1884,24 +1884,6 @@ "@testing-library/dom": ">=7.21.4" } }, - "node_modules/@tisoap/react-flow-smart-edge": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@tisoap/react-flow-smart-edge/-/react-flow-smart-edge-3.0.0.tgz", - "integrity": "sha512-XtEQT0IrOqPwJvCzgEoj3Y16/EK4SOcjZO7FmOPU+qJWmgYjeTyv7J35CGm6dFeJYdZ2gHDrvQ1zwaXuo23/8g==", - "dependencies": { - "pathfinding": "0.4.18" - }, - "engines": { - "node": ">=16", - "npm": "^8.0.0" - }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17", - "reactflow": ">=11", - "typescript": ">=4.6" - } - }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -3884,11 +3866,6 @@ "node": ">= 0.4" } }, - "node_modules/heap": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.5.tgz", - "integrity": "sha512-G7HLD+WKcrOyJP5VQwYZNC3Z6FcQ7YYjEFiFoIj8PfEr73mu421o8B1N5DKUcc8K37EsJ2XXWA8DtrDz/2dReg==" - }, "node_modules/history": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", @@ -4921,14 +4898,6 @@ "integrity": "sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==", "dev": true }, - "node_modules/pathfinding": { - "version": "0.4.18", - "resolved": "https://registry.npmjs.org/pathfinding/-/pathfinding-0.4.18.tgz", - "integrity": "sha512-R0TGEQ9GRcFCDvAWlJAWC+KGJ9SLbW4c0nuZRcioVlXVTlw+F5RvXQ8SQgSqI9KXWC1ew95vgmIiyaWTlCe9Ag==", - "dependencies": { - "heap": "0.2.5" - } - }, "node_modules/pathval": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", @@ -6028,6 +5997,7 @@ "version": "5.1.6", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6721,7 +6691,6 @@ "@eclipse-sirius/sirius-components-core": "*", "@material-ui/core": "4.12.4", "@material-ui/icons": "4.11.3", - "@tisoap/react-flow-smart-edge": "3.0.0", "elkjs": "0.8.2", "graphql": "16.8.0", "html-to-image": "1.11.11", @@ -7100,7 +7069,6 @@ "@material-ui/lab": "4.0.0-alpha.61", "@ObeoNetwork/gantt-task-react": "0.4.9", "@ObeoNetwork/react-trello": "2.4.11", - "@tisoap/react-flow-smart-edge": "3.0.0", "@types/react": "17.0.37", "@types/react-router-dom": "5.3.3", "@xstate/react": "1.6.3", @@ -8188,7 +8156,6 @@ "@testing-library/jest-dom": "5.14.1", "@testing-library/react": "12.1.2", "@testing-library/user-event": "13.2.1", - "@tisoap/react-flow-smart-edge": "3.0.0", "@types/d3": "7.0.0", "@types/jest": "27.0.0", "@types/node": "16.6.0", @@ -9188,14 +9155,6 @@ "@babel/runtime": "^7.12.5" } }, - "@tisoap/react-flow-smart-edge": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@tisoap/react-flow-smart-edge/-/react-flow-smart-edge-3.0.0.tgz", - "integrity": "sha512-XtEQT0IrOqPwJvCzgEoj3Y16/EK4SOcjZO7FmOPU+qJWmgYjeTyv7J35CGm6dFeJYdZ2gHDrvQ1zwaXuo23/8g==", - "requires": { - "pathfinding": "0.4.18" - } - }, "@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -10735,11 +10694,6 @@ "function-bind": "^1.1.2" } }, - "heap": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.5.tgz", - "integrity": "sha512-G7HLD+WKcrOyJP5VQwYZNC3Z6FcQ7YYjEFiFoIj8PfEr73mu421o8B1N5DKUcc8K37EsJ2XXWA8DtrDz/2dReg==" - }, "history": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", @@ -11532,14 +11486,6 @@ "integrity": "sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==", "dev": true }, - "pathfinding": { - "version": "0.4.18", - "resolved": "https://registry.npmjs.org/pathfinding/-/pathfinding-0.4.18.tgz", - "integrity": "sha512-R0TGEQ9GRcFCDvAWlJAWC+KGJ9SLbW4c0nuZRcioVlXVTlw+F5RvXQ8SQgSqI9KXWC1ew95vgmIiyaWTlCe9Ag==", - "requires": { - "heap": "0.2.5" - } - }, "pathval": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", @@ -12366,7 +12312,8 @@ "typescript": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", - "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==" + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "dev": true }, "ufo": { "version": "1.3.2", diff --git a/packages/diagrams/frontend/sirius-components-diagrams/package.json b/packages/diagrams/frontend/sirius-components-diagrams/package.json index 926be818cd..424e6e8431 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams/package.json +++ b/packages/diagrams/frontend/sirius-components-diagrams/package.json @@ -34,7 +34,6 @@ "@eclipse-sirius/sirius-components-core": "*", "@material-ui/core": "4.12.4", "@material-ui/icons": "4.11.3", - "@tisoap/react-flow-smart-edge": "3.0.0", "elkjs": "0.8.2", "graphql": "16.8.0", "html-to-image": "1.11.11", diff --git a/packages/diagrams/frontend/sirius-components-diagrams/src/converter/convertDiagram.ts b/packages/diagrams/frontend/sirius-components-diagrams/src/converter/convertDiagram.ts index bb5e19202f..07c1430643 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams/src/converter/convertDiagram.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams/src/converter/convertDiagram.ts @@ -23,7 +23,7 @@ import { ListLayoutStrategy, } from '../graphql/subscription/nodeFragment.types'; import { Diagram, EdgeLabel, NodeData } from '../renderer/DiagramRenderer.types'; -import { EdgeWrapperData } from '../renderer/edge/EdgeWrapper.types'; +import { MultiLabelEdgeData } from '../renderer/edge/MultiLabelEdge.types'; import { RawDiagram } from '../renderer/layout/layout.types'; import { computeBorderNodeExtents, computeBorderNodePositions } from '../renderer/layout/layoutBorderNodes'; import { layoutHandles } from '../renderer/layout/layoutHandles'; @@ -142,7 +142,7 @@ export const convertDiagram = ( const edges: Edge[] = gqlDiagram.edges.map((gqlEdge) => { const sourceNode: Node | undefined = nodeId2node.get(gqlEdge.sourceId); const targetNode: Node | undefined = nodeId2node.get(gqlEdge.targetId); - const data: EdgeWrapperData = { + const data: MultiLabelEdgeData = { targetObjectId: gqlEdge.targetObjectId, targetObjectKind: gqlEdge.targetObjectKind, targetObjectLabel: gqlEdge.targetObjectLabel, diff --git a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/DiagramRenderer.types.ts b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/DiagramRenderer.types.ts index 1e9a00d751..8b9c1e29b4 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/DiagramRenderer.types.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/DiagramRenderer.types.ts @@ -14,7 +14,7 @@ import { Edge, Node } from 'reactflow'; import { GQLNodeDescription } from '../graphql/query/nodeDescriptionFragment.types'; import { GQLDiagramRefreshedEventPayload } from '../graphql/subscription/diagramEventSubscription.types'; -import { EdgeWrapperData } from './edge/EdgeWrapper.types'; +import { MultiLabelEdgeData } from './edge/MultiLabelEdge.types'; import { ConnectionHandle } from './handles/ConnectionHandles.types'; import { DiagramNodeType } from './node/NodeTypes.types'; @@ -25,7 +25,7 @@ export interface DiagramRendererProps { export interface Diagram { metadata: DiagramMetadata; nodes: Node[]; - edges: Edge[]; + edges: Edge[]; } export interface DiagramMetadata { diff --git a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/edge/EdgeLayout.ts b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/edge/EdgeLayout.ts index f58351fc75..40d1f06ee8 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/edge/EdgeLayout.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/edge/EdgeLayout.ts @@ -22,7 +22,7 @@ import { GetUpdatedConnectionHandlesParameters, NodeCenter, } from './EdgeLayout.types'; -import { isDescendantOf, isDescendantOfId } from '../layout/layoutNode'; +import { isDescendantOf } from '../layout/layoutNode'; export const getUpdatedConnectionHandles: GetUpdatedConnectionHandlesParameters = ( sourceNode, @@ -103,9 +103,7 @@ const getParameters: GetParameters = (movingNode, nodeA, nodeB, visiblesNodes) = } const horizontalDifference = Math.abs(centerA.x - centerB.x); const verticalDifference = Math.abs(centerA.y - centerB.y); - const isDescendant = nodeA.data.isBorderNode - ? isDescendantOfId(nodeA.parentNode ?? '', nodeB, (nodeId) => visiblesNodes.find((node) => node.id === nodeId)) - : isDescendantOf(nodeB, nodeA, (nodeId) => visiblesNodes.find((node) => node.id === nodeId)); + const isDescendant = isDescendantOf(nodeB, nodeA, (nodeId) => visiblesNodes.find((node) => node.id === nodeId)); let position: Position; if (horizontalDifference > verticalDifference) { if (isDescendant) { diff --git a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/edge/EdgeTypes.ts b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/edge/EdgeTypes.ts index df5cb41b6a..112fe69b87 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/edge/EdgeTypes.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/edge/EdgeTypes.ts @@ -12,8 +12,8 @@ *******************************************************************************/ import { DiagramEdgeTypes } from './EdgeTypes.types'; -import { EdgeWrapper } from './EdgeWrapper'; +import { MultiLabelEdge } from './MultiLabelEdge'; export const edgeTypes: DiagramEdgeTypes = { - multiLabelEdge: EdgeWrapper, + multiLabelEdge: MultiLabelEdge, }; diff --git a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/edge/EdgeWrapper.tsx b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/edge/EdgeWrapper.tsx deleted file mode 100644 index 72cd29e0e9..0000000000 --- a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/edge/EdgeWrapper.tsx +++ /dev/null @@ -1,248 +0,0 @@ -/******************************************************************************* - * 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 - * https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - * - * Contributors: - * Obeo - initial API and implementation - *******************************************************************************/ -import { getSmartEdge, pathfindingJumpPointNoDiagonal, svgDrawStraightLinePath } from '@tisoap/react-flow-smart-edge'; -import { memo, useCallback, useMemo } from 'react'; -import { EdgeProps, Node, Position, ReactFlowState, getSmoothStepPath, useStore, useReactFlow } from 'reactflow'; -import { NodeData, EdgeData } from '../DiagramRenderer.types'; -import { getHandleCoordinatesByPosition } from './EdgeLayout'; -import { MultiLabelEdge } from './MultiLabelEdge'; -import { EdgeWrapperData } from './EdgeWrapper.types'; - -const roundToNearestTwenty = (num: number): number => Math.round(num / 20) * 20; - -const isAncestorOf = (child: Node, candidate: Node, nodeById: (arg0: string) => Node | undefined): boolean => { - if (child.parentNode === candidate.id) { - return true; - } else { - const childParent: Node | undefined = child.parentNode ? nodeById(child.parentNode) : undefined; - return childParent !== undefined && isAncestorOf(childParent, candidate, nodeById); - } -}; - -const getAncestors = ( - node: Node | undefined, - nodes: Node[], - maxAncestorToSearch: Node | undefined = undefined, - ancestors: Node[] = [] -): Node[] => { - if (!node) { - return ancestors; - } - ancestors.push(node); - if (node.parentNode || node.id === maxAncestorToSearch?.id) { - const parentNode = nodes.find((n) => n.id === node.parentNode); - return getAncestors(parentNode, nodes, maxAncestorToSearch, ancestors); - } else { - return ancestors; - } -}; - -const getLowestCommunAncestor = (node: Node, nodes: Node[], ancestorIds: string[]): Node | undefined => { - if (ancestorIds.includes(node.id)) { - return node; - } - if (node.parentNode) { - const parentNode = nodes.find((n) => n.id === node.parentNode); - if (parentNode) { - return getLowestCommunAncestor(parentNode, nodes, ancestorIds); - } else { - return undefined; - } - } else { - return undefined; - } -}; - -function findLowestCommonAncestor(nodes, sourceNode, targetNode) { - const sourceAncestorIds = getAncestors(sourceNode, nodes).map((ancestor) => ancestor.id); - return getLowestCommunAncestor(targetNode, nodes, sourceAncestorIds); -} - -export const EdgeWrapper = memo((props: EdgeProps) => { - const { source, target, markerEnd, markerStart, sourcePosition, targetPosition, sourceHandleId, targetHandleId } = - props; - const { getNodes } = useReactFlow(); - const nodes = getNodes(); - const sourceNode = useStore | undefined>( - useCallback((store: ReactFlowState) => store.nodeInternals.get(source), [source]) - ); - const targetNode = useStore | undefined>( - useCallback((store: ReactFlowState) => store.nodeInternals.get(target), [target]) - ); - - if (!sourceNode || !targetNode) { - return null; - } - - let { x: sourceX, y: sourceY } = getHandleCoordinatesByPosition(sourceNode, sourcePosition, sourceHandleId ?? ''); - let { x: targetX, y: targetY } = getHandleCoordinatesByPosition(targetNode, targetPosition, targetHandleId ?? ''); - - // trick to have the source of the edge positioned at the very border of a node - // if the edge has a marker, then only the marker need to touch the node - const handleSourceRadius = markerStart == undefined || markerStart.includes('None') ? 2 : 3; - switch (sourcePosition) { - case Position.Right: - sourceX = sourceX + handleSourceRadius; - sourceY = roundToNearestTwenty(sourceY); - break; - case Position.Left: - sourceX = sourceX - handleSourceRadius; - sourceY = roundToNearestTwenty(sourceY); - break; - case Position.Top: - sourceY = sourceY - handleSourceRadius; - sourceX = roundToNearestTwenty(sourceX); - break; - case Position.Bottom: - sourceY = sourceY + handleSourceRadius; - sourceX = roundToNearestTwenty(sourceX); - break; - } - // trick to have the target of the edge positioned at the very border of a node - // if the edge has a marker, then only the marker need to touch the node - const handleTargetRadius = markerEnd == undefined || markerEnd.includes('None') ? 2 : 3; - switch (targetPosition) { - case Position.Right: - targetX = targetX + handleTargetRadius; - targetY = roundToNearestTwenty(targetY); - break; - case Position.Left: - targetX = targetX - handleTargetRadius; - targetY = roundToNearestTwenty(targetY); - break; - case Position.Top: - targetY = targetY - handleTargetRadius; - targetX = roundToNearestTwenty(targetX); - break; - case Position.Bottom: - targetY = targetY + handleTargetRadius; - targetX = roundToNearestTwenty(targetX); - break; - } - - const nodeHierarchy = nodes.map((node) => node.id + node.parentNode).join(); - - const lowestCommonAncestor = useMemo(() => findLowestCommonAncestor(nodes, sourceNode, targetNode), [nodeHierarchy]); - - const sourceAncestorIds: string[] = useMemo( - () => - getAncestors( - nodes.find((n) => n.id === sourceNode.parentNode ?? ''), - nodes, - lowestCommonAncestor - ).map((node) => node.id), - [nodeHierarchy, lowestCommonAncestor, sourceNode] - ); - - const targetAncestorIds: string[] = useMemo( - () => - getAncestors( - nodes.find((n) => n.id === targetNode.parentNode ?? ''), - nodes, - lowestCommonAncestor - ).map((node) => node.id), - [nodeHierarchy, lowestCommonAncestor, targetNode] - ); - - const nodeIdsToConsider: string[] = useMemo(() => { - return nodes - .filter((node) => { - if (node.id === sourceNode.id || node.id === targetNode.id) { - return true; - } - if (sourceNode.data.isBorderNode && node.id === sourceNode.parentNode) { - return !targetAncestorIds.includes(sourceNode.parentNode); - } - if (targetNode.data.isBorderNode && node.id === targetNode.parentNode) { - return !sourceAncestorIds.includes(targetNode.parentNode); - } - const sourceAncestorSiblings = sourceAncestorIds.includes(node.parentNode ?? ''); - const targetAncestorSiblings = targetAncestorIds.includes(node.parentNode ?? ''); - const isDirectAncestor = sourceAncestorIds.includes(node.id) || targetAncestorIds.includes(node.id); - return ( - (sourceAncestorSiblings || targetAncestorSiblings || node.parentNode === lowestCommonAncestor) && - !isDirectAncestor - ); - }) - .map((node) => node.id); - }, [nodeHierarchy, sourceAncestorIds.join(), targetAncestorIds.join()]); - - const nodesPositionToConsider: string = nodes - .filter((node) => nodeIdsToConsider.includes(node.id)) - .map((node) => node.id + node.position.x + node.position.y + node.width + node.height) - .join(); - - const getSmartEdgeResponse = useMemo(() => { - const nodesToConsider: Node[] = nodes.filter((node) => nodeIdsToConsider.includes(node.id)); - return getSmartEdge({ - sourceX, - sourceY, - sourcePosition, - targetX, - targetY, - targetPosition, - nodes: nodesToConsider, - options: { - nodePadding: 5, - gridRatio: 20, - drawEdge: svgDrawStraightLinePath, - generatePath: pathfindingJumpPointNoDiagonal, - }, - }); - }, [ - nodeIdsToConsider.join(), - nodesPositionToConsider, - sourceX, - sourceY, - sourcePosition, - targetX, - targetY, - targetPosition, - ]); - - if (getSmartEdgeResponse === null) { - const [edgePath, labelX, labelY] = getSmoothStepPath({ - sourceX, - sourceY, - sourcePosition, - targetX, - targetY, - targetPosition, - }); - return ( - - ); - } - const { edgeCenterX, edgeCenterY, svgPathString } = getSmartEdgeResponse; - return ( - - ); -}); diff --git a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/edge/EdgeWrapper.types.ts b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/edge/EdgeWrapper.types.ts deleted file mode 100644 index a71bd2fdaf..0000000000 --- a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/edge/EdgeWrapper.types.ts +++ /dev/null @@ -1,19 +0,0 @@ -/******************************************************************************* - * 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 - * https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - * - * Contributors: - * Obeo - initial API and implementation - *******************************************************************************/ - -import { EdgeData, EdgeLabel } from '../DiagramRenderer.types'; - -export interface EdgeWrapperData extends EdgeData { - beginLabel?: EdgeLabel; - endLabel?: EdgeLabel; -} diff --git a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/edge/MultiLabelEdge.tsx b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/edge/MultiLabelEdge.tsx index 262e72a91c..c0641485b8 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/edge/MultiLabelEdge.tsx +++ b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/edge/MultiLabelEdge.tsx @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2024 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 @@ -10,14 +10,24 @@ * Contributors: * Obeo - initial API and implementation *******************************************************************************/ - import { getCSSColor } from '@eclipse-sirius/sirius-components-core'; import { Theme, useTheme } from '@material-ui/core/styles'; -import { memo } from 'react'; -import { BaseEdge, EdgeLabelRenderer, Position } from 'reactflow'; -import { MultiLabelEdgeProps } from './MultiLabelEdge.types'; +import { memo, useCallback } from 'react'; +import { + BaseEdge, + EdgeLabelRenderer, + EdgeProps, + Node, + Position, + ReactFlowState, + getSmoothStepPath, + useStore, +} from 'reactflow'; +import { NodeData } from '../DiagramRenderer.types'; import { Label } from '../Label'; import { DiagramElementPalette } from '../palette/DiagramElementPalette'; +import { getHandleCoordinatesByPosition } from './EdgeLayout'; +import { MultiLabelEdgeData } from './MultiLabelEdge.types'; const multiLabelEdgeStyle = ( theme: Theme, @@ -37,9 +47,12 @@ const multiLabelEdgeStyle = ( return multiLabelEdgeStyle; }; + export const MultiLabelEdge = memo( ({ id, + source, + target, data, style, markerEnd, @@ -47,16 +60,70 @@ export const MultiLabelEdge = memo( selected, sourcePosition, targetPosition, - sourceX, - sourceY, - targetX, - targetY, - edgeCenterX, - edgeCenterY, - svgPathString, - }: MultiLabelEdgeProps) => { + sourceHandleId, + targetHandleId, + }: EdgeProps) => { + const theme = useTheme(); + + const sourceNode = useStore | undefined>( + useCallback((store: ReactFlowState) => store.nodeInternals.get(source), [source]) + ); + const targetNode = useStore | undefined>( + useCallback((store: ReactFlowState) => store.nodeInternals.get(target), [target]) + ); + + if (!sourceNode || !targetNode) { + return null; + } + + let { x: sourceX, y: sourceY } = getHandleCoordinatesByPosition(sourceNode, sourcePosition, sourceHandleId ?? ''); + let { x: targetX, y: targetY } = getHandleCoordinatesByPosition(targetNode, targetPosition, targetHandleId ?? ''); + + // trick to have the source of the edge positioned at the very border of a node + // if the edge has a marker, then only the marker need to touch the node + const handleSourceRadius = markerStart == undefined || markerStart.includes('None') ? 2 : 3; + switch (sourcePosition) { + case Position.Right: + sourceX = sourceX + handleSourceRadius; + break; + case Position.Left: + sourceX = sourceX - handleSourceRadius; + break; + case Position.Top: + sourceY = sourceY - handleSourceRadius; + break; + case Position.Bottom: + sourceY = sourceY + handleSourceRadius; + break; + } + // trick to have the target of the edge positioned at the very border of a node + // if the edge has a marker, then only the marker need to touch the node + const handleTargetRadius = markerEnd == undefined || markerEnd.includes('None') ? 2 : 3; + switch (targetPosition) { + case Position.Right: + targetX = targetX + handleTargetRadius; + break; + case Position.Left: + targetX = targetX - handleTargetRadius; + break; + case Position.Top: + targetY = targetY - handleTargetRadius; + break; + case Position.Bottom: + targetY = targetY + handleTargetRadius; + break; + } + + const [edgePath, labelX, labelY] = getSmoothStepPath({ + sourceX, + sourceY, + sourcePosition, + targetX, + targetY, + targetPosition, + }); + const { beginLabel, endLabel, label, faded } = data || {}; - const theme: Theme = useTheme(); const getTranslateFromHandlePositon = (position: Position) => { switch (position) { @@ -75,7 +142,7 @@ export const MultiLabelEdge = memo( <> )} {label && ( -