From 778dcc822703d419f5080c280ff1c3606087fcda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20ROU=C3=8BN=C3=89?= Date: Thu, 10 Oct 2024 17:17:12 +0200 Subject: [PATCH] [3868] Make path of edges editable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug: https://github.com/eclipse-sirius/sirius-web/issues/3868 Signed-off-by: Florian ROUËNÉ --- CHANGELOG.adoc | 3 + package-lock.json | 26 ++++- .../diagrams/DiagramEventProcessor.java | 20 ++-- .../diagrams/dto/DiagramLayoutDataInput.java | 5 +- .../dto/DiagramLayoutDataPayload.java | 6 +- .../diagrams/dto/EdgeLayoutDataInput.java} | 14 ++- .../main/resources/schema/diagram.graphqls | 12 ++ .../diagram/DiagramLayoutDataDataFetcher.java | 11 +- .../diagrams/layoutdata/EdgeLayoutData.java | 7 +- .../sirius-components-diagrams/package.json | 4 + .../src/converter/convertDiagram.ts | 5 + .../graphql/subscription/diagramFragment.ts | 4 + .../subscription/diagramFragment.types.ts | 6 + .../src/renderer/DiagramRenderer.types.ts | 3 +- .../src/renderer/edge/BendPoint.tsx | 58 ++++++++++ .../src/renderer/edge/BendPoint.types.ts | 28 +++++ .../src/renderer/edge/MultiLabelEdge.tsx | 105 +++++++++++++++++- .../src/renderer/edge/MultiLabelEdge.types.ts | 3 +- .../renderer/edge/SmartStepEdgeWrapper.tsx | 35 +++++- .../renderer/edge/SmoothStepEdgeWrapper.tsx | 50 +++++++-- .../layout/useSynchronizeLayoutData.ts | 14 +++ .../layout/useSynchronizeLayoutData.types.ts | 6 + .../frontend/sirius-web/package.json | 2 + 23 files changed, 381 insertions(+), 46 deletions(-) rename packages/diagrams/backend/{sirius-components-diagrams/src/main/java/org/eclipse/sirius/components/diagrams/layoutdata/Ratio.java => sirius-components-collaborative-diagrams/src/main/java/org/eclipse/sirius/components/collaborative/diagrams/dto/EdgeLayoutDataInput.java} (60%) create mode 100644 packages/diagrams/frontend/sirius-components-diagrams/src/renderer/edge/BendPoint.tsx create mode 100644 packages/diagrams/frontend/sirius-components-diagrams/src/renderer/edge/BendPoint.types.ts diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index a4a9b8fd9d..19ef372948 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -44,6 +44,8 @@ This allows an error to be displayed when there is a problem during uploading - [releng] Switch to Spring Boot 3.3.3 - https://github.com/eclipse-sirius/sirius-web/issues/3846[#3846] [core] Migrate the frontend to `react 18.3.1`, `react-dom 18.3.1`, `react-router-dom 6.26.0`, `@xstate/react: 3.0.0` and `@ObeoNetwork/gantt-task-react 0.6.0` - https://github.com/eclipse-sirius/sirius-web/issues/3840[#3840] [diagram] Migrate to ReactFlow 12 +- https://github.com/eclipse-sirius/sirius-web/issues/3868[#3868] [diagram] Add dependency to `react-draggable 4.4.6` +- https://github.com/eclipse-sirius/sirius-web/issues/3868[#3868] [diagram] Add dependency to `svg-path-parser 1.1.0` === Bug fixes @@ -74,6 +76,7 @@ The new endpoints are: description (optional). ** deleteProject (`POST /api/rest/projects/{projectId}`): Delete the project with the given id (projectId). ** updateProject (`PUT /projects/{projectId}`): Update the project with the given id (projectId). +- https://github.com/eclipse-sirius/sirius-web/issues/3868[#3868] [diagram] Make path of edges editable === Improvements diff --git a/package-lock.json b/package-lock.json index 78f7968b61..928b579fe9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6189,7 +6189,6 @@ "version": "4.4.6", "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz", "integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==", - "dev": true, "dependencies": { "clsx": "^1.1.1", "prop-types": "^15.8.1" @@ -6203,7 +6202,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", - "dev": true, "engines": { "node": ">=6" } @@ -6873,6 +6871,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-path-parser": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/svg-path-parser/-/svg-path-parser-1.1.0.tgz", + "integrity": "sha512-jGCUqcQyXpfe38R7RFfhrMyfXcBmpMNJI/B+4CE9/Unkh98UporAc461GTthv+TVDuZXsBx7/WiwJb1Oh4tt4A==" + }, "node_modules/symbol-observable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", @@ -7807,7 +7810,9 @@ "prettier": "2.7.1", "react": "18.3.1", "react-dom": "18.3.1", + "react-draggable": "4.4.6", "rollup-plugin-peer-deps-external": "2.2.4", + "svg-path-parser": "1.1.0", "tss-react": "4.9.7", "typescript": "5.4.5", "vite": "5.2.11", @@ -7827,6 +7832,8 @@ "pathfinding": "0.4.18", "react": "18.3.1", "react-dom": "18.3.1", + "react-draggable": "4.4.6", + "svg-path-parser": "1.1.0", "tss-react": "4.9.7" } }, @@ -8197,8 +8204,10 @@ "prop-types": "15.8.1", "react": "18.3.1", "react-dom": "18.3.1", + "react-draggable": "4.4.6", "react-router-dom": "6.26.0", "subscriptions-transport-ws": "0.11.0", + "svg-path-parser": "1.1.0", "tss-react": "4.9.7", "xstate": "4.32.1" }, @@ -8775,7 +8784,9 @@ "prettier": "2.7.1", "react": "18.3.1", "react-dom": "18.3.1", + "react-draggable": "4.4.6", "rollup-plugin-peer-deps-external": "2.2.4", + "svg-path-parser": "1.1.0", "tss-react": "4.9.7", "typescript": "5.4.5", "vite": "5.2.11", @@ -9101,8 +9112,10 @@ "prop-types": "15.8.1", "react": "18.3.1", "react-dom": "18.3.1", + "react-draggable": "4.4.6", "react-router-dom": "6.26.0", "subscriptions-transport-ws": "0.11.0", + "svg-path-parser": "1.1.0", "tss-react": "4.9.7", "typescript": "5.4.5", "vite": "5.2.11", @@ -13290,7 +13303,6 @@ "version": "4.4.6", "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz", "integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==", - "dev": true, "requires": { "clsx": "^1.1.1", "prop-types": "^15.8.1" @@ -13299,8 +13311,7 @@ "clsx": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", - "dev": true + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" } } }, @@ -13804,6 +13815,11 @@ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "peer": true }, + "svg-path-parser": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/svg-path-parser/-/svg-path-parser-1.1.0.tgz", + "integrity": "sha512-jGCUqcQyXpfe38R7RFfhrMyfXcBmpMNJI/B+4CE9/Unkh98UporAc461GTthv+TVDuZXsBx7/WiwJb1Oh4tt4A==" + }, "symbol-observable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", diff --git a/packages/diagrams/backend/sirius-components-collaborative-diagrams/src/main/java/org/eclipse/sirius/components/collaborative/diagrams/DiagramEventProcessor.java b/packages/diagrams/backend/sirius-components-collaborative-diagrams/src/main/java/org/eclipse/sirius/components/collaborative/diagrams/DiagramEventProcessor.java index fc029ad465..b2c7dfad2d 100644 --- a/packages/diagrams/backend/sirius-components-collaborative-diagrams/src/main/java/org/eclipse/sirius/components/collaborative/diagrams/DiagramEventProcessor.java +++ b/packages/diagrams/backend/sirius-components-collaborative-diagrams/src/main/java/org/eclipse/sirius/components/collaborative/diagrams/DiagramEventProcessor.java @@ -32,6 +32,7 @@ import org.eclipse.sirius.components.collaborative.diagrams.api.IDiagramInput; import org.eclipse.sirius.components.collaborative.diagrams.api.IDiagramInputReferencePositionProvider; import org.eclipse.sirius.components.collaborative.diagrams.dto.DiagramRefreshedEventPayload; +import org.eclipse.sirius.components.collaborative.diagrams.dto.EdgeLayoutDataInput; import org.eclipse.sirius.components.collaborative.diagrams.dto.LayoutDiagramInput; import org.eclipse.sirius.components.collaborative.diagrams.dto.NodeLayoutDataInput; import org.eclipse.sirius.components.collaborative.diagrams.dto.ReferencePosition; @@ -46,6 +47,7 @@ import org.eclipse.sirius.components.diagrams.Diagram; import org.eclipse.sirius.components.diagrams.description.DiagramDescription; import org.eclipse.sirius.components.diagrams.layoutdata.DiagramLayoutData; +import org.eclipse.sirius.components.diagrams.layoutdata.EdgeLayoutData; import org.eclipse.sirius.components.diagrams.layoutdata.NodeLayoutData; import org.eclipse.sirius.components.representations.IRepresentation; import org.slf4j.Logger; @@ -141,7 +143,14 @@ public void handle(One payloadSink, Many changeDesc (oldValue, newValue) -> newValue )); - var layoutData = new DiagramLayoutData(nodeLayoutData, Map.of(), Map.of()); + var edgeLayoutData = layoutDiagramInput.diagramLayoutData().edgeLayoutData().stream() + .collect(Collectors.toMap( + EdgeLayoutDataInput::id, + edgeLayoutDataInput -> new EdgeLayoutData(edgeLayoutDataInput.id(), edgeLayoutDataInput.bendingPoints()), + (oldValue, newValue) -> newValue + )); + + var layoutData = new DiagramLayoutData(nodeLayoutData, edgeLayoutData, Map.of()); var laidOutDiagram = Diagram.newDiagram(diagram) .layoutData(layoutData) .build(); @@ -226,17 +235,13 @@ private ReferencePosition getReferencePosition(IInput diagramInput) { */ public boolean shouldRefresh(ChangeDescription changeDescription) { Diagram diagram = this.diagramContext.getDiagram(); - // @formatter:off var optionalDiagramDescription = this.representationDescriptionSearchService.findById(this.editingContext, diagram.getDescriptionId()) .filter(DiagramDescription.class::isInstance) .map(DiagramDescription.class::cast); - // @formatter:on - // @formatter:off return optionalDiagramDescription.flatMap(this.representationRefreshPolicyRegistry::getRepresentationRefreshPolicy) .orElseGet(this::getDefaultRefreshPolicy) .shouldRefresh(changeDescription); - // @formatter:on } private IRepresentationRefreshPolicy getDefaultRefreshPolicy() { @@ -254,10 +259,9 @@ private IRepresentationRefreshPolicy getDefaultRefreshPolicy() { @Override public Flux getOutputEvents(IInput input) { - // @formatter:off return Flux.merge( - this.diagramEventFlux.getFlux(this.currentRevisionId, this.currentRevisionCause), - this.subscriptionManager.getFlux(input) + this.diagramEventFlux.getFlux(this.currentRevisionId, this.currentRevisionCause), + this.subscriptionManager.getFlux(input) ); } diff --git a/packages/diagrams/backend/sirius-components-collaborative-diagrams/src/main/java/org/eclipse/sirius/components/collaborative/diagrams/dto/DiagramLayoutDataInput.java b/packages/diagrams/backend/sirius-components-collaborative-diagrams/src/main/java/org/eclipse/sirius/components/collaborative/diagrams/dto/DiagramLayoutDataInput.java index d82a82f439..52c5a02ecf 100644 --- a/packages/diagrams/backend/sirius-components-collaborative-diagrams/src/main/java/org/eclipse/sirius/components/collaborative/diagrams/dto/DiagramLayoutDataInput.java +++ b/packages/diagrams/backend/sirius-components-collaborative-diagrams/src/main/java/org/eclipse/sirius/components/collaborative/diagrams/dto/DiagramLayoutDataInput.java @@ -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 @@ -19,5 +19,6 @@ * * @author sbegaudeau */ -public record DiagramLayoutDataInput(List nodeLayoutData) { +public record DiagramLayoutDataInput(List nodeLayoutData, List edgeLayoutData) { + } diff --git a/packages/diagrams/backend/sirius-components-collaborative-diagrams/src/main/java/org/eclipse/sirius/components/collaborative/diagrams/dto/DiagramLayoutDataPayload.java b/packages/diagrams/backend/sirius-components-collaborative-diagrams/src/main/java/org/eclipse/sirius/components/collaborative/diagrams/dto/DiagramLayoutDataPayload.java index 46c639db0c..3e1fddc2f9 100644 --- a/packages/diagrams/backend/sirius-components-collaborative-diagrams/src/main/java/org/eclipse/sirius/components/collaborative/diagrams/dto/DiagramLayoutDataPayload.java +++ b/packages/diagrams/backend/sirius-components-collaborative-diagrams/src/main/java/org/eclipse/sirius/components/collaborative/diagrams/dto/DiagramLayoutDataPayload.java @@ -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 @@ -14,6 +14,7 @@ import java.util.List; +import org.eclipse.sirius.components.diagrams.layoutdata.EdgeLayoutData; import org.eclipse.sirius.components.diagrams.layoutdata.NodeLayoutData; /** @@ -21,5 +22,6 @@ * * @author sbegaudeau */ -public record DiagramLayoutDataPayload(List nodeLayoutData) { +public record DiagramLayoutDataPayload(List nodeLayoutData, List edgeLayoutData) { + } diff --git a/packages/diagrams/backend/sirius-components-diagrams/src/main/java/org/eclipse/sirius/components/diagrams/layoutdata/Ratio.java b/packages/diagrams/backend/sirius-components-collaborative-diagrams/src/main/java/org/eclipse/sirius/components/collaborative/diagrams/dto/EdgeLayoutDataInput.java similarity index 60% rename from packages/diagrams/backend/sirius-components-diagrams/src/main/java/org/eclipse/sirius/components/diagrams/layoutdata/Ratio.java rename to packages/diagrams/backend/sirius-components-collaborative-diagrams/src/main/java/org/eclipse/sirius/components/collaborative/diagrams/dto/EdgeLayoutDataInput.java index 58472118b5..a1c2e996e7 100644 --- a/packages/diagrams/backend/sirius-components-diagrams/src/main/java/org/eclipse/sirius/components/diagrams/layoutdata/Ratio.java +++ b/packages/diagrams/backend/sirius-components-collaborative-diagrams/src/main/java/org/eclipse/sirius/components/collaborative/diagrams/dto/EdgeLayoutDataInput.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023 Obeo. + * Copyright (c) 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,13 +10,17 @@ * Contributors: * Obeo - initial API and implementation *******************************************************************************/ +package org.eclipse.sirius.components.collaborative.diagrams.dto; -package org.eclipse.sirius.components.diagrams.layoutdata; +import java.util.List; + +import org.eclipse.sirius.components.diagrams.layoutdata.Position; /** - * The position ratio of an element. + * Input used to receive edge layout data. * - * @author sbegaudeau + * @author frouene */ -public record Ratio(double x, double y) { +public record EdgeLayoutDataInput(String id, List bendingPoints) { + } diff --git a/packages/diagrams/backend/sirius-components-collaborative-diagrams/src/main/resources/schema/diagram.graphqls b/packages/diagrams/backend/sirius-components-collaborative-diagrams/src/main/resources/schema/diagram.graphqls index 0cd366eefa..8a49f15149 100644 --- a/packages/diagrams/backend/sirius-components-collaborative-diagrams/src/main/resources/schema/diagram.graphqls +++ b/packages/diagrams/backend/sirius-components-collaborative-diagrams/src/main/resources/schema/diagram.graphqls @@ -38,6 +38,7 @@ type Diagram implements Representation { type DiagramLayoutData { nodeLayoutData: [NodeLayoutData!]! + edgeLayoutData: [EdgeLayoutData!]! } type NodeLayoutData { @@ -47,6 +48,11 @@ type NodeLayoutData { resizedByUser: Boolean! } +type EdgeLayoutData { + id: ID! + bendingPoints: [Position!]! +} + enum ViewModifier { Normal Faded @@ -567,6 +573,7 @@ input LayoutDiagramInput { input DiagramLayoutDataInput { nodeLayoutData: [NodeLayoutDataInput!]! + edgeLayoutData: [EdgeLayoutDataInput!]! } input NodeLayoutDataInput { @@ -576,6 +583,11 @@ input NodeLayoutDataInput { resizedByUser: Boolean! } +input EdgeLayoutDataInput { + id: ID! + bendingPoints: [PositionInput!]! +} + input PositionInput { x: Float! y: Float! diff --git a/packages/diagrams/backend/sirius-components-diagrams-graphql/src/main/java/org/eclipse/sirius/components/diagrams/graphql/datafetchers/diagram/DiagramLayoutDataDataFetcher.java b/packages/diagrams/backend/sirius-components-diagrams-graphql/src/main/java/org/eclipse/sirius/components/diagrams/graphql/datafetchers/diagram/DiagramLayoutDataDataFetcher.java index f7bb959597..5cfef18d75 100644 --- a/packages/diagrams/backend/sirius-components-diagrams-graphql/src/main/java/org/eclipse/sirius/components/diagrams/graphql/datafetchers/diagram/DiagramLayoutDataDataFetcher.java +++ b/packages/diagrams/backend/sirius-components-diagrams-graphql/src/main/java/org/eclipse/sirius/components/diagrams/graphql/datafetchers/diagram/DiagramLayoutDataDataFetcher.java @@ -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 @@ -26,6 +26,7 @@ */ @QueryDataFetcher(type = "Diagram", field = "layoutData") public class DiagramLayoutDataDataFetcher implements IDataFetcherWithFieldCoordinates { + @Override public DiagramLayoutDataPayload get(DataFetchingEnvironment environment) throws Exception { Diagram diagram = environment.getSource(); @@ -34,6 +35,12 @@ public DiagramLayoutDataPayload get(DataFetchingEnvironment environment) throws .values() .stream() .toList(); - return new DiagramLayoutDataPayload(nodeLayoutData); + + var edgeLayoutData = diagram.getLayoutData() + .edgeLayoutData() + .values() + .stream() + .toList(); + return new DiagramLayoutDataPayload(nodeLayoutData, edgeLayoutData); } } diff --git a/packages/diagrams/backend/sirius-components-diagrams/src/main/java/org/eclipse/sirius/components/diagrams/layoutdata/EdgeLayoutData.java b/packages/diagrams/backend/sirius-components-diagrams/src/main/java/org/eclipse/sirius/components/diagrams/layoutdata/EdgeLayoutData.java index 0ee6daef75..0b6580c903 100644 --- a/packages/diagrams/backend/sirius-components-diagrams/src/main/java/org/eclipse/sirius/components/diagrams/layoutdata/EdgeLayoutData.java +++ b/packages/diagrams/backend/sirius-components-diagrams/src/main/java/org/eclipse/sirius/components/diagrams/layoutdata/EdgeLayoutData.java @@ -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 @@ -21,8 +21,7 @@ */ public record EdgeLayoutData( String id, - Ratio sourceAnchorRelativePosition, - Ratio targetAnchorRelativePosition, - List routingPoints + List bendingPoints ) { + } diff --git a/packages/diagrams/frontend/sirius-components-diagrams/package.json b/packages/diagrams/frontend/sirius-components-diagrams/package.json index 8a3f66bed0..f4a55e5597 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams/package.json +++ b/packages/diagrams/frontend/sirius-components-diagrams/package.json @@ -38,6 +38,8 @@ "graphql": "16.8.1", "html-to-image": "1.11.11", "pathfinding": "0.4.18", + "svg-path-parser": "1.1.0", + "react-draggable": "4.4.6", "react": "18.3.1", "react-dom": "18.3.1", "@types/react": "18.3.3", @@ -61,6 +63,8 @@ "html-to-image": "1.11.11", "jsdom": "16.7.0", "pathfinding": "0.4.18", + "svg-path-parser": "1.1.0", + "react-draggable": "4.4.6", "prettier": "2.7.1", "react": "18.3.1", "react-dom": "18.3.1", 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 d7e83b802a..472bab6183 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams/src/converter/convertDiagram.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams/src/converter/convertDiagram.ts @@ -36,6 +36,7 @@ import { ImageNodeConverter } from './ImageNodeConverter'; import { ListNodeConverter } from './ListNodeConverter'; import { RectangleNodeConverter } from './RectangleNodeConverter'; import { convertContentStyle, convertLabelStyle } from './convertLabel'; +import { GQLEdgeLayoutData } from '../renderer/layout/useSynchronizeLayoutData.types'; const nodeDepth = (nodeId2node: Map, nodeId: string): number => { const node = nodeId2node.get(nodeId); @@ -143,6 +144,9 @@ 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 edgeLayoutData: GQLEdgeLayoutData | undefined = gqlDiagram.layoutData.edgeLayoutData.find( + (layoutData) => layoutData.id === gqlEdge.id + ); const data: MultiLabelEdgeData = { targetObjectId: gqlEdge.targetObjectId, targetObjectKind: gqlEdge.targetObjectKind, @@ -150,6 +154,7 @@ export const convertDiagram = ( label: null, faded: gqlEdge.state === GQLViewModifier.Faded, centerLabelEditable: gqlEdge.centerLabelEditable, + bendingPoints: edgeLayoutData?.bendingPoints ?? null, }; if (gqlEdge.beginLabel) { diff --git a/packages/diagrams/frontend/sirius-components-diagrams/src/graphql/subscription/diagramFragment.ts b/packages/diagrams/frontend/sirius-components-diagrams/src/graphql/subscription/diagramFragment.ts index ea99bf75b1..f36853bafe 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams/src/graphql/subscription/diagramFragment.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams/src/graphql/subscription/diagramFragment.ts @@ -30,6 +30,10 @@ fragment diagramFragment on Diagram { size { width height } resizedByUser } + edgeLayoutData { + id + bendingPoints { x y } + } } nodes { ...nodeFragment diff --git a/packages/diagrams/frontend/sirius-components-diagrams/src/graphql/subscription/diagramFragment.types.ts b/packages/diagrams/frontend/sirius-components-diagrams/src/graphql/subscription/diagramFragment.types.ts index 39a799b6c1..1f3eb7413b 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams/src/graphql/subscription/diagramFragment.types.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams/src/graphql/subscription/diagramFragment.types.ts @@ -25,6 +25,7 @@ export interface GQLDiagram { export interface GQLDiagramLayoutData { nodeLayoutData: GQLNodeLayoutData[]; + edgeLayoutData: GQLEdgeLayoutData[]; } export interface GQLNodeLayoutData { @@ -34,6 +35,11 @@ export interface GQLNodeLayoutData { resizedByUser: boolean; } +export interface GQLEdgeLayoutData { + id: string; + bendingPoints: GQLPosition[]; +} + export interface GQLRepresentationMetadata { kind: string; label: string; 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 a041720815..6030d1be16 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 @@ -11,7 +11,7 @@ * Obeo - initial API and implementation *******************************************************************************/ -import { Edge, Node, ReactFlowProps } from '@xyflow/react'; +import { Edge, Node, ReactFlowProps, XYPosition } from '@xyflow/react'; import { GQLNodeDescription } from '../graphql/query/nodeDescriptionFragment.types'; import { GQLDiagramRefreshedEventPayload } from '../graphql/subscription/diagramEventSubscription.types'; import { MultiLabelEdgeData } from './edge/MultiLabelEdge.types'; @@ -70,6 +70,7 @@ export interface EdgeData extends Record { label: EdgeLabel | null; faded: boolean; centerLabelEditable: boolean; + bendingPoints: XYPosition[] | null; } export interface InsideLabel { diff --git a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/edge/BendPoint.tsx b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/edge/BendPoint.tsx new file mode 100644 index 0000000000..efcb6863f5 --- /dev/null +++ b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/edge/BendPoint.tsx @@ -0,0 +1,58 @@ +/******************************************************************************* + * Copyright (c) 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 { useRef } from 'react'; +import { useViewport } from '@xyflow/react'; +import Draggable, { DraggableData } from 'react-draggable'; +import { BendPointProps, TemporaryBendPointProps } from './BendPoint.types'; + +export const BendPoint = ({ x, y, index, onDragStop, onDoubleClick }: BendPointProps) => { + const { zoom } = useViewport(); + const nodeRef = useRef(null); + + return ( + onDragStop(eventData, index)} + nodeRef={nodeRef}> + onDoubleClick(index)} + /> + + ); +}; + +export const TemporaryBendPoint = ({ x, y, index, onDragStop }: TemporaryBendPointProps) => { + const { zoom } = useViewport(); + const nodeRef = useRef(null); + return ( + onDragStop(eventData, index)} + nodeRef={nodeRef}> + + + ); +}; diff --git a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/edge/BendPoint.types.ts b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/edge/BendPoint.types.ts new file mode 100644 index 0000000000..da545a68f2 --- /dev/null +++ b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/edge/BendPoint.types.ts @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright (c) 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 { DraggableData } from 'react-draggable'; + +export interface BendPointProps { + x: number; + y: number; + index: number; + onDragStop: (eventData: DraggableData, index: number) => void; + onDoubleClick: (index: number) => void; +} + +export interface TemporaryBendPointProps { + x: number; + y: number; + index: number; + onDragStop: (eventData: DraggableData, index: number) => void; +} 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 f9fd234a3a..0b03568f91 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 @@ -13,10 +13,19 @@ import { getCSSColor } from '@eclipse-sirius/sirius-components-core'; import { Theme, useTheme } from '@mui/material/styles'; -import { BaseEdge, Edge, EdgeLabelRenderer, Position } from '@xyflow/react'; -import { memo, useMemo } from 'react'; +import { BaseEdge, Edge, EdgeLabelRenderer, Node, Position, XYPosition } from '@xyflow/react'; +import { memo, useContext, useMemo } from 'react'; +import { DraggableData } from 'react-draggable'; import { Label } from '../Label'; +import { NodeData } from '../DiagramRenderer.types'; +import { RawDiagram } from '../layout/layout.types'; +import { useSynchronizeLayoutData } from '../layout/useSynchronizeLayoutData'; +import { DiagramNodeType } from '../node/NodeTypes.types'; import { DiagramElementPalette } from '../palette/DiagramElementPalette'; +import { DiagramContext } from '../../contexts/DiagramContext'; +import { DiagramContextValue } from '../../contexts/DiagramContext.types'; +import { useStore } from '../../representation/useStore'; +import { BendPoint, TemporaryBendPoint } from './BendPoint'; import { MultiLabelEdgeData, MultiLabelEdgeProps } from './MultiLabelEdge.types'; const multiLabelEdgeStyle = ( @@ -60,6 +69,10 @@ const labelContainerStyle = (transform: string): React.CSSProperties => { }; }; +const getMiddlePoint = (p1: XYPosition, p2: XYPosition): XYPosition => { + return { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2 }; +}; + export const MultiLabelEdge = memo( ({ id, @@ -77,14 +90,85 @@ export const MultiLabelEdge = memo( edgeCenterX, edgeCenterY, svgPathString, + bendingPoints, }: MultiLabelEdgeProps>) => { const { beginLabel, endLabel, label, faded } = data || {}; const theme = useTheme(); + const { getEdges, setEdges, getNodes } = useStore(); + const { refreshEventPayloadId } = useContext(DiagramContext); + const { synchronizeLayoutData } = useSynchronizeLayoutData(); const edgeStyle = useMemo(() => multiLabelEdgeStyle(theme, style, selected, faded), [style, selected, faded]); const sourceLabelTranslation = useMemo(() => getTranslateFromHandlePositon(sourcePosition), [sourcePosition]); const targetLabelTranslation = useMemo(() => getTranslateFromHandlePositon(targetPosition), [targetPosition]); + const onDragStop = (eventData: DraggableData, index: number) => { + const edges = getEdges(); + const edge = edges.find((edge) => edge.id === id); + if (bendingPoints) { + bendingPoints[index] = { + x: eventData.x, + y: eventData.y, + }; + } + if (edge?.data) { + edge.data.bendingPoints = bendingPoints ?? null; + } + setEdges(edges); + const finalDiagram: RawDiagram = { + nodes: [...getNodes()] as Node[], + edges: edges, + }; + synchronizeLayoutData(refreshEventPayloadId, finalDiagram); + }; + + const onTemporaryDragStop = (eventData: DraggableData, index: number) => { + const edges = getEdges(); + const edge = edges.find((edge) => edge.id === id); + if (edge?.data?.bendingPoints) { + edge.data.bendingPoints.splice(index, 0, { + x: eventData.x, + y: eventData.y, + }); + setEdges(edges); + const finalDiagram: RawDiagram = { + nodes: [...getNodes()] as Node[], + edges: edges, + }; + synchronizeLayoutData(refreshEventPayloadId, finalDiagram); + } + }; + + const onDoubleClick = (index: number) => { + const edges = getEdges(); + const edge = edges.find((edge) => edge.id === id); + if (edge?.data?.bendingPoints) { + edge.data.bendingPoints.splice(index, 1); + setEdges(edges); + const finalDiagram: RawDiagram = { + nodes: [...getNodes()] as Node[], + edges: edges, + }; + synchronizeLayoutData(refreshEventPayloadId, finalDiagram); + } + }; + + const middlePoints: XYPosition[] = []; + if (bendingPoints) { + if (bendingPoints.length > 0) { + for (let i = 0; i < bendingPoints.length; i++) { + const p1 = i === 0 ? { x: sourceX, y: sourceY } : bendingPoints[i - 1]; + const p2 = bendingPoints[i]; + if (p1 && p2) { + middlePoints.push(getMiddlePoint(p1, p2)); + } + } + middlePoints.push(getMiddlePoint(bendingPoints[bendingPoints.length - 1]!, { x: targetX, y: targetY })); + } else { + middlePoints.push(getMiddlePoint({ x: sourceX, y: sourceY }, { x: targetX, y: targetY })); + } + } + return ( <> ) : null} + {selected && + bendingPoints && + bendingPoints.map((point, index) => ( + + ))} + {selected && + middlePoints && + middlePoints.map((point, index) => ( + + ))} {beginLabel && (
diff --git a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/edge/MultiLabelEdge.types.ts b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/edge/MultiLabelEdge.types.ts index 96aae07ce7..7e18983340 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/edge/MultiLabelEdge.types.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/edge/MultiLabelEdge.types.ts @@ -10,13 +10,14 @@ * Contributors: * Obeo - initial API and implementation *******************************************************************************/ -import { Edge, EdgeProps } from '@xyflow/react'; +import { Edge, EdgeProps, XYPosition } from '@xyflow/react'; import { EdgeData, EdgeLabel } from '../DiagramRenderer.types'; export type MultiLabelEdgeProps, string | undefined>> = { edgeCenterX: number; edgeCenterY: number; svgPathString: string; + bendingPoints?: XYPosition[]; } & EdgeProps; export interface MultiLabelEdgeData extends EdgeData { diff --git a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/edge/SmartStepEdgeWrapper.tsx b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/edge/SmartStepEdgeWrapper.tsx index 3cffc6d192..d45d9e15f8 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/edge/SmartStepEdgeWrapper.tsx +++ b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/edge/SmartStepEdgeWrapper.tsx @@ -20,8 +20,10 @@ import { useInternalNode, useReactFlow, useStoreApi, + XYPosition, } from '@xyflow/react'; import { memo, useContext, useMemo } from 'react'; +import parse from 'svg-path-parser'; import { NodeTypeContext } from '../../contexts/NodeContext'; import { NodeTypeContextValue } from '../../contexts/NodeContext.types'; import { EdgeData, NodeData } from '../DiagramRenderer.types'; @@ -86,8 +88,17 @@ const isNodeInternal = (node: InternalNode> | undefined): node is }; export const SmartStepEdgeWrapper = memo((props: EdgeProps>) => { - const { source, target, markerEnd, markerStart, sourcePosition, targetPosition, sourceHandleId, targetHandleId } = - props; + const { + source, + target, + markerEnd, + markerStart, + sourcePosition, + targetPosition, + sourceHandleId, + targetHandleId, + data, + } = props; const { nodeLayoutHandlers } = useContext(NodeTypeContext); const { getNodes } = useReactFlow, Edge>(); const storeApi = useStoreApi, Edge>(); @@ -266,6 +277,23 @@ export const SmartStepEdgeWrapper = memo((props: EdgeProps segment.code === 'Q') + .map((segment) => { + return { x: (segment.x + segment.x1) / 2, y: (segment.y + segment.y1) / 2 }; + }); + } return ( ); }); diff --git a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/edge/SmoothStepEdgeWrapper.tsx b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/edge/SmoothStepEdgeWrapper.tsx index 6b25c47455..4b5abd57c7 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/edge/SmoothStepEdgeWrapper.tsx +++ b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/edge/SmoothStepEdgeWrapper.tsx @@ -10,8 +10,9 @@ * Contributors: * Obeo - initial API and implementation *******************************************************************************/ -import { Edge, EdgeProps, Node, Position, getSmoothStepPath, useInternalNode } from '@xyflow/react'; +import { Edge, EdgeProps, Node, Position, getSmoothStepPath, useInternalNode, XYPosition } from '@xyflow/react'; import { memo, useContext } from 'react'; +import parse from 'svg-path-parser'; import { NodeTypeContext } from '../../contexts/NodeContext'; import { NodeTypeContextValue } from '../../contexts/NodeContext.types'; import { NodeData } from '../DiagramRenderer.types'; @@ -21,8 +22,17 @@ import { MultiLabelEdge } from './MultiLabelEdge'; import { MultiLabelEdgeData } from './MultiLabelEdge.types'; export const SmoothStepEdgeWrapper = memo((props: EdgeProps>) => { - const { source, target, markerEnd, markerStart, sourcePosition, targetPosition, sourceHandleId, targetHandleId } = - props; + const { + source, + target, + markerEnd, + markerStart, + sourcePosition, + targetPosition, + sourceHandleId, + targetHandleId, + data, + } = props; const { nodeLayoutHandlers } = useContext(NodeTypeContext); const sourceNode = useInternalNode>(source); @@ -87,14 +97,31 @@ export const SmoothStepEdgeWrapper = memo((props: EdgeProps segment.code === 'Q') + .map((segment) => { + return { x: (segment.x + segment.x1) / 2, y: (segment.y + segment.y1) / 2 }; + }); + } return ( ); }); diff --git a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/layout/useSynchronizeLayoutData.ts b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/layout/useSynchronizeLayoutData.ts index c753345bbd..30e38f0141 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/layout/useSynchronizeLayoutData.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/layout/useSynchronizeLayoutData.ts @@ -27,6 +27,7 @@ import { GQLNodeLayoutData, GQLSuccessPayload, UseSynchronizeLayoutDataValue, + GQLEdgeLayoutData, } from './useSynchronizeLayoutData.types'; const layoutDiagramMutation = gql` @@ -78,6 +79,7 @@ export const useSynchronizeLayoutData = (): UseSynchronizeLayoutDataValue => { const toDiagramLayoutData = (diagram: RawDiagram): GQLDiagramLayoutData => { const nodeLayoutData: GQLNodeLayoutData[] = []; + const edgeLayoutData: GQLEdgeLayoutData[] = []; diagram.nodes.forEach((node) => { const { @@ -102,8 +104,20 @@ export const useSynchronizeLayoutData = (): UseSynchronizeLayoutDataValue => { }); } }); + + diagram.edges.forEach((edge) => { + if (edge.data?.bendingPoints) { + edgeLayoutData.push({ + id: edge.id, + bendingPoints: edge.data.bendingPoints.map((point) => { + return { x: point.x, y: point.y }; + }), + }); + } + }); return { nodeLayoutData, + edgeLayoutData, }; }; diff --git a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/layout/useSynchronizeLayoutData.types.ts b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/layout/useSynchronizeLayoutData.types.ts index f15704cec8..af360c5472 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/layout/useSynchronizeLayoutData.types.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/layout/useSynchronizeLayoutData.types.ts @@ -20,6 +20,7 @@ export interface UseSynchronizeLayoutDataValue { export interface GQLDiagramLayoutData { nodeLayoutData: GQLNodeLayoutData[]; + edgeLayoutData: GQLEdgeLayoutData[]; } export interface GQLNodeLayoutData { @@ -29,6 +30,11 @@ export interface GQLNodeLayoutData { resizedByUser: boolean; } +export interface GQLEdgeLayoutData { + id: string; + bendingPoints: GQLPosition[]; +} + export interface GQLSize { width: number; height: number; diff --git a/packages/sirius-web/frontend/sirius-web/package.json b/packages/sirius-web/frontend/sirius-web/package.json index 4c419bc76b..50d695cd5b 100644 --- a/packages/sirius-web/frontend/sirius-web/package.json +++ b/packages/sirius-web/frontend/sirius-web/package.json @@ -39,6 +39,8 @@ "html-to-image": "1.11.11", "notistack": "3.0.1", "pathfinding": "0.4.18", + "svg-path-parser": "1.1.0", + "react-draggable": "4.4.6", "prop-types": "15.8.1", "react": "18.3.1", "react-dom": "18.3.1",