diff --git a/rsconcept/frontend/src/dialogs/DlgShowAST/ASTFlow.tsx b/rsconcept/frontend/src/dialogs/DlgShowAST/ASTFlow.tsx new file mode 100644 index 00000000..a9064d80 --- /dev/null +++ b/rsconcept/frontend/src/dialogs/DlgShowAST/ASTFlow.tsx @@ -0,0 +1,74 @@ +'use client'; + +import { useLayoutEffect } from 'react'; +import { Edge, MarkerType, Node, ReactFlow, useEdgesState, useNodesState, useReactFlow } from 'reactflow'; + +import { SyntaxTree } from '@/models/rslang'; + +import { ASTEdgeTypes } from './graph/ASTEdgeTypes'; +import { applyLayout } from './graph/ASTLayout'; +import { ASTNodeTypes } from './graph/ASTNodeTypes'; + +interface ASTFlowProps { + data: SyntaxTree; + onNodeEnter: (node: Node) => void; + onNodeLeave: (node: Node) => void; +} + +function ASTFlow({ data, onNodeEnter, onNodeLeave }: ASTFlowProps) { + const [nodes, setNodes, onNodesChange] = useNodesState([]); + const [edges, setEdges] = useEdgesState([]); + const flow = useReactFlow(); + + useLayoutEffect(() => { + const newNodes = data.map(node => ({ + id: String(node.uid), + data: node, + position: { x: 0, y: 0 }, + type: 'token' + })); + + const newEdges: Edge[] = []; + data.forEach(node => { + if (node.parent !== node.uid) { + newEdges.push({ + id: String(node.uid), + source: String(node.parent), + target: String(node.uid), + type: 'dynamic', + focusable: false, + markerEnd: { + type: MarkerType.ArrowClosed, + width: 20, + height: 20 + } + }); + } + }); + + applyLayout(newNodes, newEdges); + + setNodes(newNodes); + setEdges(newEdges); + }, [data, setNodes, setEdges, flow]); + + return ( + onNodeEnter(node)} + onNodeMouseLeave={(_, node) => onNodeLeave(node)} + onNodesChange={onNodesChange} + nodeTypes={ASTNodeTypes} + edgeTypes={ASTEdgeTypes} + fitView + maxZoom={2} + minZoom={0.5} + nodesConnectable={false} + /> + ); +} + +export default ASTFlow; diff --git a/rsconcept/frontend/src/dialogs/DlgShowAST.tsx b/rsconcept/frontend/src/dialogs/DlgShowAST/DlgShowAST.tsx similarity index 51% rename from rsconcept/frontend/src/dialogs/DlgShowAST.tsx rename to rsconcept/frontend/src/dialogs/DlgShowAST/DlgShowAST.tsx index c08490b7..66c9a2f1 100644 --- a/rsconcept/frontend/src/dialogs/DlgShowAST.tsx +++ b/rsconcept/frontend/src/dialogs/DlgShowAST/DlgShowAST.tsx @@ -1,17 +1,16 @@ 'use client'; import { useCallback, useMemo, useState } from 'react'; +import { ReactFlowProvider } from 'reactflow'; +import { Node } from 'reactflow'; -import GraphUI, { GraphEdge, GraphNode } from '@/components/ui/GraphUI'; import Modal, { ModalProps } from '@/components/ui/Modal'; import Overlay from '@/components/ui/Overlay'; import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { HelpTopic } from '@/models/miscellaneous'; import { SyntaxTree } from '@/models/rslang'; -import { graphDarkT, graphLightT } from '@/styling/color'; -import { colorBgSyntaxTree } from '@/styling/color'; -import { resources } from '@/utils/constants'; -import { labelSyntaxTree } from '@/utils/labels'; + +import ASTFlow from './ASTFlow'; interface DlgShowASTProps extends Pick { syntaxTree: SyntaxTree; @@ -19,36 +18,11 @@ interface DlgShowASTProps extends Pick { } function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) { - const { darkMode, colors } = useConceptOptions(); + const { colors } = useConceptOptions(); const [hoverID, setHoverID] = useState(undefined); const hoverNode = useMemo(() => syntaxTree.find(node => node.uid === hoverID), [hoverID, syntaxTree]); - const nodes: GraphNode[] = useMemo( - () => - syntaxTree.map(node => ({ - id: String(syntaxTree.length - node.uid), // invert order of IDs to force correct ordering in graph layout - label: labelSyntaxTree(node), - fill: colorBgSyntaxTree(node, colors) - })), - [syntaxTree, colors] - ); - - const edges: GraphEdge[] = useMemo(() => { - const result: GraphEdge[] = []; - syntaxTree.forEach(node => { - if (node.parent !== node.uid) { - result.push({ - id: String(node.uid), - source: String(syntaxTree.length - node.parent), - target: String(syntaxTree.length - node.uid) - }); - } - }); - return result; - }, [syntaxTree]); - - const handleHoverIn = useCallback((node: GraphNode) => setHoverID(syntaxTree.length - Number(node.id)), [syntaxTree]); - + const handleHoverIn = useCallback((node: Node) => setHoverID(Number(node.id)), []); const handleHoverOut = useCallback(() => setHoverID(undefined), []); return ( @@ -72,18 +46,9 @@ function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) { ) : null} -
- -
+ + + ); } diff --git a/rsconcept/frontend/src/dialogs/DlgShowAST/graph/ASTEdge.tsx b/rsconcept/frontend/src/dialogs/DlgShowAST/graph/ASTEdge.tsx new file mode 100644 index 00000000..5794bf98 --- /dev/null +++ b/rsconcept/frontend/src/dialogs/DlgShowAST/graph/ASTEdge.tsx @@ -0,0 +1,28 @@ +import { EdgeProps, getStraightPath } from 'reactflow'; + +const NODE_RADIUS = 20; +const EDGE_RADIUS = 25; + +function ASTEdge({ id, markerEnd, style, ...props }: EdgeProps) { + const scale = + EDGE_RADIUS / + Math.sqrt( + Math.pow(props.sourceX - props.targetX, 2) + + Math.pow(Math.abs(props.sourceY - props.targetY) + 2 * NODE_RADIUS, 2) + ); + + const [path] = getStraightPath({ + sourceX: props.sourceX - (props.sourceX - props.targetX) * scale, + sourceY: props.sourceY - (props.sourceY - props.targetY - 2 * NODE_RADIUS) * scale - NODE_RADIUS, + targetX: props.targetX + (props.sourceX - props.targetX) * scale, + targetY: props.targetY + (props.sourceY - props.targetY - 2 * NODE_RADIUS) * scale + NODE_RADIUS + }); + + return ( + <> + + + ); +} + +export default ASTEdge; diff --git a/rsconcept/frontend/src/dialogs/DlgShowAST/graph/ASTEdgeTypes.ts b/rsconcept/frontend/src/dialogs/DlgShowAST/graph/ASTEdgeTypes.ts new file mode 100644 index 00000000..c88a8d33 --- /dev/null +++ b/rsconcept/frontend/src/dialogs/DlgShowAST/graph/ASTEdgeTypes.ts @@ -0,0 +1,7 @@ +import { EdgeTypes } from 'reactflow'; + +import ASTEdge from './ASTEdge'; + +export const ASTEdgeTypes: EdgeTypes = { + dynamic: ASTEdge +}; diff --git a/rsconcept/frontend/src/dialogs/DlgShowAST/graph/ASTLayout.ts b/rsconcept/frontend/src/dialogs/DlgShowAST/graph/ASTLayout.ts new file mode 100644 index 00000000..a848614e --- /dev/null +++ b/rsconcept/frontend/src/dialogs/DlgShowAST/graph/ASTLayout.ts @@ -0,0 +1,35 @@ +import dagre from '@dagrejs/dagre'; +import { Edge, Node } from 'reactflow'; + +import { ISyntaxTreeNode } from '@/models/rslang'; + +const NODE_WIDTH = 44; +const NODE_HEIGHT = 44; +const HOR_SEPARATION = 40; +const VERT_SEPARATION = 40; + +export function applyLayout(nodes: Node[], edges: Edge[]) { + const dagreGraph = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({})); + dagreGraph.setGraph({ + rankdir: 'TB', + ranksep: VERT_SEPARATION, + nodesep: HOR_SEPARATION, + ranker: 'network-simplex', + align: undefined + }); + nodes.forEach(node => { + dagreGraph.setNode(node.id, { width: NODE_WIDTH, height: NODE_HEIGHT }); + }); + + edges.forEach(edge => { + dagreGraph.setEdge(edge.source, edge.target); + }); + + dagre.layout(dagreGraph); + + nodes.forEach(node => { + const nodeWithPosition = dagreGraph.node(node.id); + node.position.x = nodeWithPosition.x - NODE_WIDTH / 2; + node.position.y = nodeWithPosition.y - NODE_HEIGHT / 2; + }); +} diff --git a/rsconcept/frontend/src/dialogs/DlgShowAST/graph/ASTNode.tsx b/rsconcept/frontend/src/dialogs/DlgShowAST/graph/ASTNode.tsx new file mode 100644 index 00000000..a8ecb234 --- /dev/null +++ b/rsconcept/frontend/src/dialogs/DlgShowAST/graph/ASTNode.tsx @@ -0,0 +1,44 @@ +'use client'; + +import { useMemo } from 'react'; +import { Handle, Position } from 'reactflow'; + +import { useConceptOptions } from '@/context/ConceptOptionsContext'; +import { ISyntaxTreeNode } from '@/models/rslang'; +import { colorBgSyntaxTree } from '@/styling/color'; +import { labelSyntaxTree } from '@/utils/labels'; + +/** + * Represents graph AST node internal data. + */ +interface ASTNodeInternal { + id: string; + data: ISyntaxTreeNode; + dragging: boolean; + xPos: number; + yPos: number; +} + +function ASTNode(node: ASTNodeInternal) { + const { colors } = useConceptOptions(); + const label = useMemo(() => labelSyntaxTree(node.data), [node.data]); + + return ( + <> + +
+ +
3 ? 12 : 14 }} + > + {label} +
+ + ); +} + +export default ASTNode; diff --git a/rsconcept/frontend/src/dialogs/DlgShowAST/graph/ASTNodeTypes.ts b/rsconcept/frontend/src/dialogs/DlgShowAST/graph/ASTNodeTypes.ts new file mode 100644 index 00000000..5e7b89d9 --- /dev/null +++ b/rsconcept/frontend/src/dialogs/DlgShowAST/graph/ASTNodeTypes.ts @@ -0,0 +1,7 @@ +import { NodeTypes } from 'reactflow'; + +import ASTNode from './ASTNode'; + +export const ASTNodeTypes: NodeTypes = { + token: ASTNode +}; diff --git a/rsconcept/frontend/src/dialogs/DlgShowAST/index.tsx b/rsconcept/frontend/src/dialogs/DlgShowAST/index.tsx new file mode 100644 index 00000000..248e4db8 --- /dev/null +++ b/rsconcept/frontend/src/dialogs/DlgShowAST/index.tsx @@ -0,0 +1 @@ +export { default } from './DlgShowAST'; diff --git a/rsconcept/frontend/src/dialogs/DlgShowTypeGraph/MGraphFlow.tsx b/rsconcept/frontend/src/dialogs/DlgShowTypeGraph/MGraphFlow.tsx index 7d0e69ba..db2df632 100644 --- a/rsconcept/frontend/src/dialogs/DlgShowTypeGraph/MGraphFlow.tsx +++ b/rsconcept/frontend/src/dialogs/DlgShowTypeGraph/MGraphFlow.tsx @@ -4,7 +4,6 @@ import { useLayoutEffect } from 'react'; import { Edge, ReactFlow, useEdgesState, useNodesState, useReactFlow } from 'reactflow'; import { TMGraph } from '@/models/TMGraph'; -import { PARAMETER } from '@/utils/constants'; import { TMGraphEdgeTypes } from './graph/MGraphEdgeTypes'; import { applyLayout } from './graph/MGraphLayout'; @@ -53,7 +52,6 @@ function MGraphFlow({ data }: MGraphFlowProps) { setNodes(newNodes); setEdges(newEdges); - flow.fitView({ duration: PARAMETER.zoomDuration }); }, [data, setNodes, setEdges, flow]); return ( @@ -69,8 +67,6 @@ function MGraphFlow({ data }: MGraphFlowProps) { maxZoom={2} minZoom={0.5} nodesConnectable={false} - snapToGrid={true} - snapGrid={[PARAMETER.ossGridSize, PARAMETER.ossGridSize]} /> ); } diff --git a/rsconcept/frontend/src/dialogs/DlgShowTypeGraph/graph/MGraphNode.tsx b/rsconcept/frontend/src/dialogs/DlgShowTypeGraph/graph/MGraphNode.tsx index 5922715e..2692478f 100644 --- a/rsconcept/frontend/src/dialogs/DlgShowTypeGraph/graph/MGraphNode.tsx +++ b/rsconcept/frontend/src/dialogs/DlgShowTypeGraph/graph/MGraphNode.tsx @@ -4,10 +4,21 @@ import { useMemo } from 'react'; import { Handle, Position } from 'reactflow'; import { useConceptOptions } from '@/context/ConceptOptionsContext'; -import { MGraphNodeInternal } from '@/models/miscellaneous'; +import { TMGraphNode } from '@/models/TMGraph'; import { colorBgTMGraphNode } from '@/styling/color'; import { globals } from '@/utils/constants'; +/** + * Represents graph TMGraph node internal data. + */ +interface MGraphNodeInternal { + id: string; + data: TMGraphNode; + dragging: boolean; + xPos: number; + yPos: number; +} + function MGraphNode(node: MGraphNodeInternal) { const { colors } = useConceptOptions(); diff --git a/rsconcept/frontend/src/models/miscellaneous.ts b/rsconcept/frontend/src/models/miscellaneous.ts index fe0b4e34..79e3c204 100644 --- a/rsconcept/frontend/src/models/miscellaneous.ts +++ b/rsconcept/frontend/src/models/miscellaneous.ts @@ -6,7 +6,6 @@ import { EdgeProps, Node } from 'reactflow'; import { LibraryItemType, LocationHead } from './library'; import { IOperation } from './oss'; -import { TMGraphNode } from './TMGraph'; import { UserID } from './user'; /** @@ -46,17 +45,6 @@ export interface OssNodeInternal { yPos: number; } -/** - * Represents graph TMGraph node internal data. - */ -export interface MGraphNodeInternal { - id: string; - data: TMGraphNode; - dragging: boolean; - xPos: number; - yPos: number; -} - /** * Represents graph TMGraph edge internal data. */ diff --git a/rsconcept/frontend/src/pages/ManualsPage/items/ui/HelpFormulaTree.tsx b/rsconcept/frontend/src/pages/ManualsPage/items/ui/HelpFormulaTree.tsx index b8d7fd23..575aa488 100644 --- a/rsconcept/frontend/src/pages/ManualsPage/items/ui/HelpFormulaTree.tsx +++ b/rsconcept/frontend/src/pages/ManualsPage/items/ui/HelpFormulaTree.tsx @@ -28,7 +28,7 @@ function HelpFormulaTree() { присвоение и итерация
  • - составные выражения + составные выражения
  • ); diff --git a/rsconcept/frontend/src/styling/color.ts b/rsconcept/frontend/src/styling/color.ts index 04bd8c81..9f2d550b 100644 --- a/rsconcept/frontend/src/styling/color.ts +++ b/rsconcept/frontend/src/styling/color.ts @@ -371,7 +371,7 @@ export function colorBgSyntaxTree(node: ISyntaxTreeNode, colors: IColorTheme): s case TokenID.NT_FUNC_CALL: case TokenID.NT_ARGUMENTS: case TokenID.NT_RECURSIVE_SHORT: - return ''; + return colors.bgDisabled; case TokenID.ASSIGN: case TokenID.ITERATE: diff --git a/rsconcept/frontend/src/styling/overrides.css b/rsconcept/frontend/src/styling/overrides.css index cc6788df..fac75bb1 100644 --- a/rsconcept/frontend/src/styling/overrides.css +++ b/rsconcept/frontend/src/styling/overrides.css @@ -126,7 +126,8 @@ height: 40px; } -.react-flow__node-step { +.react-flow__node-step, +.react-flow__node-token { cursor: default; border-radius: 100%; diff --git a/rsconcept/frontend/src/utils/labels.ts b/rsconcept/frontend/src/utils/labels.ts index 78934562..03a9131e 100644 --- a/rsconcept/frontend/src/utils/labels.ts +++ b/rsconcept/frontend/src/utils/labels.ts @@ -581,16 +581,16 @@ export function labelSyntaxTree(node: ISyntaxTreeNode): string { case TokenID.NT_TUPLE: return 'TUPLE'; case TokenID.NT_ENUMERATION: return 'ENUM'; - case TokenID.NT_ENUM_DECL: return 'ENUM_DECLARATION'; - case TokenID.NT_TUPLE_DECL: return 'TUPLE_DECLARATION'; + case TokenID.NT_ENUM_DECL: return 'ENUM_DECLARE'; + case TokenID.NT_TUPLE_DECL: return 'TUPLE_DECLARE'; case TokenID.PUNCTUATION_DEFINE: return 'DEFINITION'; - case TokenID.PUNCTUATION_STRUCT: return 'STRUCTURE_DEFINITION'; + case TokenID.PUNCTUATION_STRUCT: return 'STRUCTURE_DEFINE'; case TokenID.NT_ARG_DECL: return 'ARG'; case TokenID.NT_FUNC_CALL: return 'CALL'; case TokenID.NT_ARGUMENTS: return 'ARGS'; - case TokenID.NT_FUNC_DEFINITION: return 'FUNCTION_DEFINITION'; + case TokenID.NT_FUNC_DEFINITION: return 'FUNCTION_DEFINE'; case TokenID.NT_RECURSIVE_SHORT: return labelToken(TokenID.NT_RECURSIVE_FULL);