diff --git a/frontend/src/components/visual-editor/hooks/useVisualEditor.tsx b/frontend/src/components/visual-editor/hooks/useVisualEditor.tsx index 6fd146d..18dd47a 100644 --- a/frontend/src/components/visual-editor/hooks/useVisualEditor.tsx +++ b/frontend/src/components/visual-editor/hooks/useVisualEditor.tsx @@ -8,10 +8,7 @@ */ import { debounce } from "@mui/material"; -import createEngine, { - DefaultLinkModel, - DiagramModel, -} from "@projectstorm/react-diagrams"; +import createEngine, { DiagramModel } from "@projectstorm/react-diagrams"; import * as React from "react"; import { createContext, useContext } from "react"; @@ -26,6 +23,10 @@ import { } from "@/types/visual-editor.types"; import { ZOOM_LEVEL } from "../constants"; +import { + AdvancedLinkFactory, + AdvancedLinkModel, +} from "../v2/AdvancedLink/AdvancedLink"; import { CustomCanvasWidget } from "../v2/CustomCanvasWidget"; import { CustomDeleteItemsAction } from "../v2/CustomDiagramNodes/CustomDeleteAction"; import { NodeFactory } from "../v2/CustomDiagramNodes/NodeFactory"; @@ -87,6 +88,8 @@ const buildDiagram = ({ model = new DiagramModel(); engine.getNodeFactories().registerFactory(new NodeFactory()); + engine.getLinkFactories().registerFactory(new AdvancedLinkFactory()); + engine .getActionEventBus() .registerAction(new CustomDeleteItemsAction({ callback: onRemoveNode })); @@ -139,12 +142,12 @@ const buildDiagram = ({ } } }; - const links: DefaultLinkModel[] = []; + const links: AdvancedLinkModel[] = []; data.forEach((datum, index) => { if ("nextBlocks" in datum && Array.isArray(datum.nextBlocks)) { datum.nextBlocks?.forEach((nextBlock) => { - const link = new DefaultLinkModel(); + const link = new AdvancedLinkModel(); const sourceNode = nodes[index]; const targetNode = nodes.find( // @ts-ignore @@ -164,7 +167,7 @@ const buildDiagram = ({ //recursive link if ("attachedBlock" in datum && datum.attachedBlock) { - const link = new DefaultLinkModel({ + const link = new AdvancedLinkModel({ color: "#019185", selectedColor: "#019185", type: "default", diff --git a/frontend/src/components/visual-editor/v2/AdvancedLink/AdvancedLink.tsx b/frontend/src/components/visual-editor/v2/AdvancedLink/AdvancedLink.tsx new file mode 100644 index 0000000..fbf933f --- /dev/null +++ b/frontend/src/components/visual-editor/v2/AdvancedLink/AdvancedLink.tsx @@ -0,0 +1,103 @@ +import { css, keyframes } from "@emotion/react"; +import styled from "@emotion/styled"; +import { + DefaultLinkFactory, + DefaultLinkModel, + DefaultLinkModelOptions, + DefaultLinkWidget, +} from "@projectstorm/react-diagrams"; +import React from "react"; + +interface Point { + x: number; + y: number; +} +const createCurvedPath = (start: Point, end: Point) => { + const controlPoint1X = start.x + 220; + const controlPoint1Y = start.y - 250; + const controlPoint2X = end.x - 250; + const controlPoint2Y = end.y - 250; + + return `M ${start.x},${start.y} C ${controlPoint1X},${controlPoint1Y} ${controlPoint2X},${controlPoint2Y} ${end.x},${end.y}`; +}; + +namespace S { + export const Keyframes = keyframes` + from { + stroke-dashoffset: 24; + } + to { + stroke-dashoffset: 0; + } + `; + + const selected = css` + stroke-dasharray: 10, 2; + animation: ${Keyframes} 1s linear infinite; + `; + + export const Path = styled.path<{ selected: boolean }>` + ${(p) => p.selected && selected}; + fill: none; + pointer-events: auto; + `; +} + +export class AdvancedLinkModel extends DefaultLinkModel { + constructor(options?: DefaultLinkModelOptions) { + super({ + ...options, + type: "advanced", + }); + } +} + +export class AdvancedLinkFactory extends DefaultLinkFactory { + constructor() { + super("advanced"); + } + + generateModel(): AdvancedLinkModel { + return new AdvancedLinkModel(); + } + + generateReactWidget(event): JSX.Element { + return ; + } + + generateLinkSegment( + model: AdvancedLinkModel, + selected: boolean, + path: string, + ) { + const isSelfLoop = + model.getSourcePort().getNode() === model.getTargetPort().getNode(); + + if (isSelfLoop) { + // Adjust the path to create a curve + const sourcePortPosition = model.getSourcePort().getPosition(); + const targetPortPosition = model.getTargetPort().getPosition(); + const startPoint: Point = { + x: sourcePortPosition.x + 20, + y: sourcePortPosition.y + 20, + }; + const endPoint: Point = { + x: targetPortPosition.x + 20, + y: targetPortPosition.y + 20, + }; + + path = createCurvedPath(startPoint, endPoint); + } + + return ( + + ); + } +} diff --git a/frontend/src/components/visual-editor/v2/Diagrams.tsx b/frontend/src/components/visual-editor/v2/Diagrams.tsx index 431a408..3b5aee3 100644 --- a/frontend/src/components/visual-editor/v2/Diagrams.tsx +++ b/frontend/src/components/visual-editor/v2/Diagrams.tsx @@ -25,7 +25,6 @@ import { tabsClasses, } from "@mui/material"; import { - DefaultLinkModel, DefaultPortModel, DiagramEngine, DiagramModel, @@ -54,6 +53,7 @@ import { IBlock } from "@/types/block.types"; import { ICategory } from "@/types/category.types"; import { BlockPorts } from "@/types/visual-editor.types"; +import { AdvancedLinkModel } from "./AdvancedLink/AdvancedLink"; import BlockDialog from "../BlockDialog"; import { ZOOM_LEVEL } from "../constants"; import { useVisualEditor } from "../hooks/useVisualEditor"; @@ -195,7 +195,7 @@ const Diagrams = () => { entity, port, }: { - entity: DefaultLinkModel; + entity: AdvancedLinkModel; port: DefaultPortModel; }) => { const link = model.getLink(entity.getOptions().id as string); @@ -205,7 +205,10 @@ const Diagrams = () => { [BlockPorts.nextBlocksOutPort, BlockPorts.attachmentOutPort].includes( // @ts-expect-error protected attr entity.targetPort.getOptions().label, - ) + ) || + (link.getSourcePort().getType() === "attached" && + link.getSourcePort().getParent().getOptions().id === + link.getTargetPort().getParent().getOptions().id) ) { model.removeLink(link);