Compare commits
11 Commits
b03e2033eb
...
a2615a9236
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a2615a9236 | ||
![]() |
fc9d21d425 | ||
![]() |
4012b8f995 | ||
![]() |
a63af2b5cf | ||
![]() |
82731be327 | ||
![]() |
eb97dc5974 | ||
![]() |
f6bb76f5e1 | ||
![]() |
80f34d90c4 | ||
![]() |
39c972eeea | ||
![]() |
01b1ade339 | ||
![]() |
34212a5c07 |
|
@ -31,7 +31,7 @@ This readme file is used mostly to document project dependencies and conventions
|
||||||
- axios
|
- axios
|
||||||
- clsx
|
- clsx
|
||||||
- react-icons
|
- react-icons
|
||||||
- react-router-dom
|
- react-router
|
||||||
- react-toastify
|
- react-toastify
|
||||||
- react-loader-spinner
|
- react-loader-spinner
|
||||||
- react-tabs
|
- react-tabs
|
||||||
|
@ -44,7 +44,6 @@ This readme file is used mostly to document project dependencies and conventions
|
||||||
- js-file-download
|
- js-file-download
|
||||||
- use-debounce
|
- use-debounce
|
||||||
- framer-motion
|
- framer-motion
|
||||||
- reagraph
|
|
||||||
- html-to-image
|
- html-to-image
|
||||||
- @tanstack/react-table
|
- @tanstack/react-table
|
||||||
- @uiw/react-codemirror
|
- @uiw/react-codemirror
|
||||||
|
|
1945
rsconcept/frontend/package-lock.json
generated
1945
rsconcept/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -17,49 +17,48 @@
|
||||||
"@tanstack/react-table": "^8.20.5",
|
"@tanstack/react-table": "^8.20.5",
|
||||||
"@uiw/codemirror-themes": "^4.23.6",
|
"@uiw/codemirror-themes": "^4.23.6",
|
||||||
"@uiw/react-codemirror": "^4.23.6",
|
"@uiw/react-codemirror": "^4.23.6",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.8",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"eslint-plugin-react-hooks": "^5.0.0",
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
"framer-motion": "^11.11.17",
|
"framer-motion": "^11.12.0",
|
||||||
"html-to-image": "^1.11.11",
|
"html-to-image": "^1.11.11",
|
||||||
"js-file-download": "^0.4.12",
|
"js-file-download": "^0.4.12",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-error-boundary": "^4.1.2",
|
"react-error-boundary": "^4.1.2",
|
||||||
"react-icons": "^5.3.0",
|
"react-icons": "^5.3.0",
|
||||||
"react-intl": "^6.8.9",
|
"react-intl": "^7.0.1",
|
||||||
"react-loader-spinner": "^6.1.6",
|
"react-loader-spinner": "^6.1.6",
|
||||||
"react-router-dom": "^6.28.0",
|
"react-router": "^7.0.1",
|
||||||
"react-select": "^5.8.3",
|
"react-select": "^5.8.3",
|
||||||
"react-tabs": "^6.0.2",
|
"react-tabs": "^6.0.2",
|
||||||
"react-toastify": "^10.0.6",
|
"react-toastify": "^10.0.6",
|
||||||
"react-tooltip": "^5.28.0",
|
"react-tooltip": "^5.28.0",
|
||||||
"react-zoom-pan-pinch": "^3.6.1",
|
"react-zoom-pan-pinch": "^3.6.1",
|
||||||
"reactflow": "^11.11.4",
|
"reactflow": "^11.11.4",
|
||||||
"reagraph": "^4.20.1",
|
|
||||||
"use-debounce": "^10.0.4"
|
"use-debounce": "^10.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@lezer/generator": "^1.7.1",
|
"@lezer/generator": "^1.7.1",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^22.9.1",
|
"@types/node": "^22.10.1",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.12",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.0.1",
|
"@typescript-eslint/eslint-plugin": "^8.0.1",
|
||||||
"@typescript-eslint/parser": "^8.0.1",
|
"@typescript-eslint/parser": "^8.0.1",
|
||||||
"@vitejs/plugin-react": "^4.3.3",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.16.0",
|
||||||
"eslint-plugin-react": "^7.37.2",
|
"eslint-plugin-react": "^7.37.2",
|
||||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
"globals": "^15.12.0",
|
"globals": "^15.13.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"postcss": "^8.4.49",
|
"postcss": "^8.4.49",
|
||||||
"tailwindcss": "^3.4.15",
|
"tailwindcss": "^3.4.15",
|
||||||
"ts-jest": "^29.2.5",
|
"ts-jest": "^29.2.5",
|
||||||
"typescript": "^5.6.3",
|
"typescript": "^5.7.2",
|
||||||
"typescript-eslint": "^8.15.0",
|
"typescript-eslint": "^8.16.0",
|
||||||
"vite": "^5.4.11"
|
"vite": "^6.0.2"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"preset": "ts-jest",
|
"preset": "ts-jest",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Outlet } from 'react-router-dom';
|
import { Outlet } from 'react-router';
|
||||||
|
|
||||||
import ConceptToaster from '@/app/ConceptToaster';
|
import ConceptToaster from '@/app/ConceptToaster';
|
||||||
import Footer from '@/app/Footer';
|
import Footer from '@/app/Footer';
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { createBrowserRouter } from 'react-router-dom';
|
import { createBrowserRouter } from 'react-router';
|
||||||
|
|
||||||
import CreateItemPage from '@/pages/CreateItemPage';
|
import CreateItemPage from '@/pages/CreateItemPage';
|
||||||
import DatabaseSchemaPage from '@/pages/DatabaseSchemaPage';
|
import DatabaseSchemaPage from '@/pages/DatabaseSchemaPage';
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { RouterProvider } from 'react-router-dom';
|
import { RouterProvider } from 'react-router';
|
||||||
|
|
||||||
import { Router } from './Router';
|
import { Router } from './Router';
|
||||||
|
|
||||||
|
|
|
@ -139,6 +139,7 @@ function PickMultiConstituenta({
|
||||||
graph={foldedGraph}
|
graph={foldedGraph}
|
||||||
isCore={cstID => isBasicConcept(schema.cstByID.get(cstID)?.cst_type)}
|
isCore={cstID => isBasicConcept(schema.cstByID.get(cstID)?.cst_type)}
|
||||||
isOwned={cstID => !schema.cstByID.get(cstID)?.is_inherited}
|
isOwned={cstID => !schema.cstByID.get(cstID)?.is_inherited}
|
||||||
|
selected={selected}
|
||||||
setSelected={setSelected}
|
setSelected={setSelected}
|
||||||
emptySelection={selected.length === 0}
|
emptySelection={selected.length === 0}
|
||||||
className='w-fit'
|
className='w-fit'
|
||||||
|
|
|
@ -19,15 +19,17 @@ import MiniButton from '../ui/MiniButton';
|
||||||
|
|
||||||
interface ToolbarGraphSelectionProps extends CProps.Styling {
|
interface ToolbarGraphSelectionProps extends CProps.Styling {
|
||||||
graph: Graph;
|
graph: Graph;
|
||||||
|
selected: number[];
|
||||||
isCore: (item: number) => boolean;
|
isCore: (item: number) => boolean;
|
||||||
isOwned: (item: number) => boolean;
|
isOwned?: (item: number) => boolean;
|
||||||
setSelected: React.Dispatch<React.SetStateAction<number[]>>;
|
setSelected: (newSelection: number[]) => void;
|
||||||
emptySelection?: boolean;
|
emptySelection?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ToolbarGraphSelection({
|
function ToolbarGraphSelection({
|
||||||
className,
|
className,
|
||||||
graph,
|
graph,
|
||||||
|
selected,
|
||||||
isCore,
|
isCore,
|
||||||
isOwned,
|
isOwned,
|
||||||
setSelected,
|
setSelected,
|
||||||
|
@ -40,13 +42,13 @@ function ToolbarGraphSelection({
|
||||||
}, [setSelected, graph, isCore]);
|
}, [setSelected, graph, isCore]);
|
||||||
|
|
||||||
const handleSelectOwned = useCallback(
|
const handleSelectOwned = useCallback(
|
||||||
() => setSelected([...graph.nodes.keys()].filter(isOwned)),
|
() => (isOwned ? setSelected([...graph.nodes.keys()].filter(isOwned)) : undefined),
|
||||||
[setSelected, graph, isOwned]
|
[setSelected, graph, isOwned]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleInvertSelection = useCallback(
|
const handleInvertSelection = useCallback(
|
||||||
() => setSelected(prev => [...graph.nodes.keys()].filter(item => !prev.includes(item))),
|
() => setSelected([...graph.nodes.keys()].filter(item => !selected.includes(item))),
|
||||||
[setSelected, graph]
|
[setSelected, selected, graph]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -60,31 +62,31 @@ function ToolbarGraphSelection({
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml='Выделить все влияющие'
|
titleHtml='Выделить все влияющие'
|
||||||
icon={<IconGraphCollapse size='1.25rem' className='icon-primary' />}
|
icon={<IconGraphCollapse size='1.25rem' className='icon-primary' />}
|
||||||
onClick={() => setSelected(prev => [...prev, ...graph.expandAllInputs(prev)])}
|
onClick={() => setSelected([...selected, ...graph.expandAllInputs(selected)])}
|
||||||
disabled={emptySelection}
|
disabled={emptySelection}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml='Выделить все зависимые'
|
titleHtml='Выделить все зависимые'
|
||||||
icon={<IconGraphExpand size='1.25rem' className='icon-primary' />}
|
icon={<IconGraphExpand size='1.25rem' className='icon-primary' />}
|
||||||
onClick={() => setSelected(prev => [...prev, ...graph.expandAllOutputs(prev)])}
|
onClick={() => setSelected([...selected, ...graph.expandAllOutputs(selected)])}
|
||||||
disabled={emptySelection}
|
disabled={emptySelection}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml='<b>Максимизация</b> <br/>дополнение выделения конституентами, <br/>зависимыми только от выделенных'
|
titleHtml='<b>Максимизация</b> <br/>дополнение выделения конституентами, <br/>зависимыми только от выделенных'
|
||||||
icon={<IconGraphMaximize size='1.25rem' className='icon-primary' />}
|
icon={<IconGraphMaximize size='1.25rem' className='icon-primary' />}
|
||||||
onClick={() => setSelected(prev => graph.maximizePart(prev))}
|
onClick={() => setSelected(graph.maximizePart(selected))}
|
||||||
disabled={emptySelection}
|
disabled={emptySelection}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml='Выделить поставщиков'
|
titleHtml='Выделить поставщиков'
|
||||||
icon={<IconGraphInputs size='1.25rem' className='icon-primary' />}
|
icon={<IconGraphInputs size='1.25rem' className='icon-primary' />}
|
||||||
onClick={() => setSelected(prev => [...prev, ...graph.expandInputs(prev)])}
|
onClick={() => setSelected([...selected, ...graph.expandInputs(selected)])}
|
||||||
disabled={emptySelection}
|
disabled={emptySelection}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml='Выделить потребителей'
|
titleHtml='Выделить потребителей'
|
||||||
icon={<IconGraphOutputs size='1.25rem' className='icon-primary' />}
|
icon={<IconGraphOutputs size='1.25rem' className='icon-primary' />}
|
||||||
onClick={() => setSelected(prev => [...prev, ...graph.expandOutputs(prev)])}
|
onClick={() => setSelected([...selected, ...graph.expandOutputs(selected)])}
|
||||||
disabled={emptySelection}
|
disabled={emptySelection}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
|
@ -97,11 +99,13 @@ function ToolbarGraphSelection({
|
||||||
icon={<IconGraphCore size='1.25rem' className='icon-primary' />}
|
icon={<IconGraphCore size='1.25rem' className='icon-primary' />}
|
||||||
onClick={handleSelectCore}
|
onClick={handleSelectCore}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
{isOwned ? (
|
||||||
titleHtml='Выделить собственные'
|
<MiniButton
|
||||||
icon={<IconPredecessor size='1.25rem' className='icon-primary' />}
|
titleHtml='Выделить собственные'
|
||||||
onClick={handleSelectOwned}
|
icon={<IconPredecessor size='1.25rem' className='icon-primary' />}
|
||||||
/>
|
onClick={handleSelectOwned}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
36
rsconcept/frontend/src/components/ui/Flow/DynamicEdge.tsx
Normal file
36
rsconcept/frontend/src/components/ui/Flow/DynamicEdge.tsx
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import { EdgeProps, getStraightPath } from 'reactflow';
|
||||||
|
|
||||||
|
import { PARAMETER } from '@/utils/constants';
|
||||||
|
|
||||||
|
const RADIUS = PARAMETER.graphNodeRadius + PARAMETER.graphNodePadding;
|
||||||
|
|
||||||
|
function DynamicEdge({ id, markerEnd, style, ...props }: EdgeProps) {
|
||||||
|
const sourceY = props.sourceY - PARAMETER.graphNodeRadius - PARAMETER.graphHandleSize;
|
||||||
|
const targetY = props.targetY + PARAMETER.graphNodeRadius + PARAMETER.graphHandleSize;
|
||||||
|
|
||||||
|
const dx = props.targetX - props.sourceX;
|
||||||
|
const dy = targetY - sourceY;
|
||||||
|
const distance = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2));
|
||||||
|
|
||||||
|
if (distance <= 2 * RADIUS) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ux = dx / distance;
|
||||||
|
const uy = dy / distance;
|
||||||
|
|
||||||
|
const [path] = getStraightPath({
|
||||||
|
sourceX: props.sourceX + ux * RADIUS,
|
||||||
|
sourceY: sourceY + uy * RADIUS,
|
||||||
|
targetX: props.targetX - ux * RADIUS,
|
||||||
|
targetY: targetY - uy * RADIUS
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<path id={id} className='react-flow__edge-path' d={path} markerEnd={markerEnd} style={style} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DynamicEdge;
|
|
@ -1,21 +0,0 @@
|
||||||
// Reexporting necessary reagraph types.
|
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { GraphCanvas as GraphUI } from 'reagraph';
|
|
||||||
|
|
||||||
export {
|
|
||||||
type CollapseProps,
|
|
||||||
type GraphCanvasRef,
|
|
||||||
type GraphEdge,
|
|
||||||
type GraphNode,
|
|
||||||
Sphere,
|
|
||||||
useSelection
|
|
||||||
} from 'reagraph';
|
|
||||||
export { type LayoutTypes as GraphLayout } from 'reagraph';
|
|
||||||
|
|
||||||
import { ThreeEvent } from '@react-three/fiber';
|
|
||||||
|
|
||||||
export type GraphMouseEvent = ThreeEvent<MouseEvent>;
|
|
||||||
export type GraphPointerEvent = ThreeEvent<PointerEvent>;
|
|
||||||
|
|
||||||
export default GraphUI;
|
|
|
@ -98,7 +98,10 @@ function Modal({
|
||||||
return (
|
return (
|
||||||
<div className='fixed top-0 left-0 w-full h-full z-modal cursor-default'>
|
<div className='fixed top-0 left-0 w-full h-full z-modal cursor-default'>
|
||||||
<div className={clsx('z-navigation', 'fixed top-0 left-0', 'w-full h-full', 'cc-modal-blur')} />
|
<div className={clsx('z-navigation', 'fixed top-0 left-0', 'w-full h-full', 'cc-modal-blur')} />
|
||||||
<div className={clsx('z-navigation', 'fixed top-0 left-0', 'w-full h-full', 'cc-modal-backdrop')} />
|
<div
|
||||||
|
className={clsx('z-navigation', 'fixed top-0 left-0', 'w-full h-full', 'cc-modal-backdrop')}
|
||||||
|
onClick={hideWindow}
|
||||||
|
/>
|
||||||
<motion.div
|
<motion.div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router';
|
||||||
|
|
||||||
interface TextURLProps {
|
interface TextURLProps {
|
||||||
/** Text to display. */
|
/** Text to display. */
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router';
|
||||||
|
|
||||||
import { globals } from '@/utils/constants';
|
import { globals } from '@/utils/constants';
|
||||||
import { contextOutsideScope } from '@/utils/labels';
|
import { contextOutsideScope } from '@/utils/labels';
|
||||||
|
@ -54,7 +54,7 @@ export const NavigationState = ({ children }: React.PropsWithChildren) => {
|
||||||
}
|
}
|
||||||
if (validate()) {
|
if (validate()) {
|
||||||
scrollTop();
|
scrollTop();
|
||||||
router(path);
|
Promise.resolve(router(path)).catch(console.log);
|
||||||
setIsBlocked(false);
|
setIsBlocked(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -65,7 +65,7 @@ export const NavigationState = ({ children }: React.PropsWithChildren) => {
|
||||||
(path: string) => {
|
(path: string) => {
|
||||||
if (validate()) {
|
if (validate()) {
|
||||||
scrollTop();
|
scrollTop();
|
||||||
router(path, { replace: true });
|
Promise.resolve(router(path, { replace: true })).catch(console.log);
|
||||||
setIsBlocked(false);
|
setIsBlocked(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -75,7 +75,7 @@ export const NavigationState = ({ children }: React.PropsWithChildren) => {
|
||||||
const back = useCallback(() => {
|
const back = useCallback(() => {
|
||||||
if (validate()) {
|
if (validate()) {
|
||||||
scrollTop();
|
scrollTop();
|
||||||
router(-1);
|
Promise.resolve(router(-1)).catch(console.log);
|
||||||
setIsBlocked(false);
|
setIsBlocked(false);
|
||||||
}
|
}
|
||||||
}, [router, validate, scrollTop]);
|
}, [router, validate, scrollTop]);
|
||||||
|
@ -83,7 +83,7 @@ export const NavigationState = ({ children }: React.PropsWithChildren) => {
|
||||||
const forward = useCallback(() => {
|
const forward = useCallback(() => {
|
||||||
if (validate()) {
|
if (validate()) {
|
||||||
scrollTop();
|
scrollTop();
|
||||||
router(1);
|
Promise.resolve(router(1)).catch(console.log);
|
||||||
setIsBlocked(false);
|
setIsBlocked(false);
|
||||||
}
|
}
|
||||||
}, [router, validate, scrollTop]);
|
}, [router, validate, scrollTop]);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useLayoutEffect } from 'react';
|
import { useLayoutEffect } from 'react';
|
||||||
import { Edge, MarkerType, Node, ReactFlow, useEdgesState, useNodesState, useReactFlow } from 'reactflow';
|
import { Edge, MarkerType, Node, ReactFlow, useEdgesState, useNodesState } from 'reactflow';
|
||||||
|
|
||||||
import { SyntaxTree } from '@/models/rslang';
|
import { SyntaxTree } from '@/models/rslang';
|
||||||
|
|
||||||
|
@ -13,12 +13,12 @@ interface ASTFlowProps {
|
||||||
data: SyntaxTree;
|
data: SyntaxTree;
|
||||||
onNodeEnter: (node: Node) => void;
|
onNodeEnter: (node: Node) => void;
|
||||||
onNodeLeave: (node: Node) => void;
|
onNodeLeave: (node: Node) => void;
|
||||||
|
onChangeDragging: (value: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ASTFlow({ data, onNodeEnter, onNodeLeave }: ASTFlowProps) {
|
function ASTFlow({ data, onNodeEnter, onNodeLeave, onChangeDragging }: ASTFlowProps) {
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||||
const [edges, setEdges] = useEdgesState([]);
|
const [edges, setEdges] = useEdgesState([]);
|
||||||
const flow = useReactFlow();
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const newNodes = data.map(node => ({
|
const newNodes = data.map(node => ({
|
||||||
|
@ -50,7 +50,7 @@ function ASTFlow({ data, onNodeEnter, onNodeLeave }: ASTFlowProps) {
|
||||||
|
|
||||||
setNodes(newNodes);
|
setNodes(newNodes);
|
||||||
setEdges(newEdges);
|
setEdges(newEdges);
|
||||||
}, [data, setNodes, setEdges, flow]);
|
}, [data, setNodes, setEdges]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
|
@ -60,6 +60,8 @@ function ASTFlow({ data, onNodeEnter, onNodeLeave }: ASTFlowProps) {
|
||||||
nodesFocusable={false}
|
nodesFocusable={false}
|
||||||
onNodeMouseEnter={(_, node) => onNodeEnter(node)}
|
onNodeMouseEnter={(_, node) => onNodeEnter(node)}
|
||||||
onNodeMouseLeave={(_, node) => onNodeLeave(node)}
|
onNodeMouseLeave={(_, node) => onNodeLeave(node)}
|
||||||
|
onNodeDragStart={() => onChangeDragging(true)}
|
||||||
|
onNodeDragStop={() => onChangeDragging(false)}
|
||||||
onNodesChange={onNodesChange}
|
onNodesChange={onNodesChange}
|
||||||
nodeTypes={ASTNodeTypes}
|
nodeTypes={ASTNodeTypes}
|
||||||
edgeTypes={ASTEdgeTypes}
|
edgeTypes={ASTEdgeTypes}
|
||||||
|
|
|
@ -25,6 +25,8 @@ function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) {
|
||||||
const handleHoverIn = useCallback((node: Node) => setHoverID(Number(node.id)), []);
|
const handleHoverIn = useCallback((node: Node) => setHoverID(Number(node.id)), []);
|
||||||
const handleHoverOut = useCallback(() => setHoverID(undefined), []);
|
const handleHoverOut = useCallback(() => setHoverID(undefined), []);
|
||||||
|
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
readonly
|
readonly
|
||||||
|
@ -37,8 +39,8 @@ function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) {
|
||||||
className='px-2 py-1 rounded-2xl cc-blur max-w-[60ch] text-lg text-center'
|
className='px-2 py-1 rounded-2xl cc-blur max-w-[60ch] text-lg text-center'
|
||||||
style={{ backgroundColor: colors.bgBlur }}
|
style={{ backgroundColor: colors.bgBlur }}
|
||||||
>
|
>
|
||||||
{!hoverNode ? expression : null}
|
{!hoverNode || isDragging ? expression : null}
|
||||||
{hoverNode ? (
|
{!isDragging && hoverNode ? (
|
||||||
<div>
|
<div>
|
||||||
<span>{expression.slice(0, hoverNode.start)}</span>
|
<span>{expression.slice(0, hoverNode.start)}</span>
|
||||||
<span className='clr-selected'>{expression.slice(hoverNode.start, hoverNode.finish)}</span>
|
<span className='clr-selected'>{expression.slice(hoverNode.start, hoverNode.finish)}</span>
|
||||||
|
@ -47,7 +49,12 @@ function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) {
|
||||||
) : null}
|
) : null}
|
||||||
</Overlay>
|
</Overlay>
|
||||||
<ReactFlowProvider>
|
<ReactFlowProvider>
|
||||||
<ASTFlow data={syntaxTree} onNodeEnter={handleHoverIn} onNodeLeave={handleHoverOut} />
|
<ASTFlow
|
||||||
|
data={syntaxTree}
|
||||||
|
onNodeEnter={handleHoverIn}
|
||||||
|
onNodeLeave={handleHoverOut}
|
||||||
|
onChangeDragging={setIsDragging}
|
||||||
|
/>
|
||||||
</ReactFlowProvider>
|
</ReactFlowProvider>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
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 (
|
|
||||||
<>
|
|
||||||
<path id={id} className='react-flow__edge-path' d={path} markerEnd={markerEnd} style={style} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ASTEdge;
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { EdgeTypes } from 'reactflow';
|
import { EdgeTypes } from 'reactflow';
|
||||||
|
|
||||||
import ASTEdge from './ASTEdge';
|
import DynamicEdge from '@/components/ui/Flow/DynamicEdge';
|
||||||
|
|
||||||
export const ASTEdgeTypes: EdgeTypes = {
|
export const ASTEdgeTypes: EdgeTypes = {
|
||||||
dynamic: ASTEdge
|
dynamic: DynamicEdge
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,23 +2,19 @@ import dagre from '@dagrejs/dagre';
|
||||||
import { Edge, Node } from 'reactflow';
|
import { Edge, Node } from 'reactflow';
|
||||||
|
|
||||||
import { ISyntaxTreeNode } from '@/models/rslang';
|
import { ISyntaxTreeNode } from '@/models/rslang';
|
||||||
|
import { PARAMETER } from '@/utils/constants';
|
||||||
const NODE_WIDTH = 44;
|
|
||||||
const NODE_HEIGHT = 44;
|
|
||||||
const HOR_SEPARATION = 40;
|
|
||||||
const VERT_SEPARATION = 40;
|
|
||||||
|
|
||||||
export function applyLayout(nodes: Node<ISyntaxTreeNode>[], edges: Edge[]) {
|
export function applyLayout(nodes: Node<ISyntaxTreeNode>[], edges: Edge[]) {
|
||||||
const dagreGraph = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
const dagreGraph = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
||||||
dagreGraph.setGraph({
|
dagreGraph.setGraph({
|
||||||
rankdir: 'TB',
|
rankdir: 'TB',
|
||||||
ranksep: VERT_SEPARATION,
|
ranksep: 40,
|
||||||
nodesep: HOR_SEPARATION,
|
nodesep: 40,
|
||||||
ranker: 'network-simplex',
|
ranker: 'network-simplex',
|
||||||
align: undefined
|
align: undefined
|
||||||
});
|
});
|
||||||
nodes.forEach(node => {
|
nodes.forEach(node => {
|
||||||
dagreGraph.setNode(node.id, { width: NODE_WIDTH, height: NODE_HEIGHT });
|
dagreGraph.setNode(node.id, { width: 2 * PARAMETER.graphNodeRadius, height: 2 * PARAMETER.graphNodeRadius });
|
||||||
});
|
});
|
||||||
|
|
||||||
edges.forEach(edge => {
|
edges.forEach(edge => {
|
||||||
|
@ -29,7 +25,7 @@ export function applyLayout(nodes: Node<ISyntaxTreeNode>[], edges: Edge[]) {
|
||||||
|
|
||||||
nodes.forEach(node => {
|
nodes.forEach(node => {
|
||||||
const nodeWithPosition = dagreGraph.node(node.id);
|
const nodeWithPosition = dagreGraph.node(node.id);
|
||||||
node.position.x = nodeWithPosition.x - NODE_WIDTH / 2;
|
node.position.x = nodeWithPosition.x - PARAMETER.graphNodeRadius;
|
||||||
node.position.y = nodeWithPosition.y - NODE_HEIGHT / 2;
|
node.position.y = nodeWithPosition.y - PARAMETER.graphNodeRadius;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,11 @@ import { ISyntaxTreeNode } from '@/models/rslang';
|
||||||
import { colorBgSyntaxTree } from '@/styling/color';
|
import { colorBgSyntaxTree } from '@/styling/color';
|
||||||
import { labelSyntaxTree } from '@/utils/labels';
|
import { labelSyntaxTree } from '@/utils/labels';
|
||||||
|
|
||||||
|
const FONT_SIZE_MAX = 14;
|
||||||
|
const FONT_SIZE_MED = 12;
|
||||||
|
|
||||||
|
const LABEL_THRESHOLD = 3;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents graph AST node internal data.
|
* Represents graph AST node internal data.
|
||||||
*/
|
*/
|
||||||
|
@ -15,6 +20,7 @@ interface ASTNodeInternal {
|
||||||
id: string;
|
id: string;
|
||||||
data: ISyntaxTreeNode;
|
data: ISyntaxTreeNode;
|
||||||
dragging: boolean;
|
dragging: boolean;
|
||||||
|
selected: boolean;
|
||||||
xPos: number;
|
xPos: number;
|
||||||
yPos: number;
|
yPos: number;
|
||||||
}
|
}
|
||||||
|
@ -32,10 +38,19 @@ function ASTNode(node: ASTNodeInternal) {
|
||||||
/>
|
/>
|
||||||
<Handle type='source' position={Position.Bottom} style={{ opacity: 0 }} />
|
<Handle type='source' position={Position.Bottom} style={{ opacity: 0 }} />
|
||||||
<div
|
<div
|
||||||
className='font-math mt-1 w-fit px-1 text-center translate-x-[calc(-50%+20px)]'
|
className='font-math mt-1 w-fit text-center translate-x-[calc(-50%+20px)]'
|
||||||
style={{ backgroundColor: colors.bgDefault, fontSize: label.length > 3 ? 12 : 14 }}
|
style={{ fontSize: label.length > LABEL_THRESHOLD ? FONT_SIZE_MED : FONT_SIZE_MAX }}
|
||||||
>
|
>
|
||||||
{label}
|
<div className='absolute top-0 left-0 text-center w-full'>{label}</div>
|
||||||
|
<div
|
||||||
|
aria-hidden='true'
|
||||||
|
style={{
|
||||||
|
WebkitTextStrokeWidth: 2,
|
||||||
|
WebkitTextStrokeColor: colors.bgDefault
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useLayoutEffect } from 'react';
|
import { useLayoutEffect } from 'react';
|
||||||
import { Edge, ReactFlow, useEdgesState, useNodesState, useReactFlow } from 'reactflow';
|
import { Edge, ReactFlow, useEdgesState, useNodesState } from 'reactflow';
|
||||||
|
|
||||||
import { TMGraph } from '@/models/TMGraph';
|
import { TMGraph } from '@/models/TMGraph';
|
||||||
|
|
||||||
|
@ -9,6 +9,9 @@ import { TMGraphEdgeTypes } from './graph/MGraphEdgeTypes';
|
||||||
import { applyLayout } from './graph/MGraphLayout';
|
import { applyLayout } from './graph/MGraphLayout';
|
||||||
import { TMGraphNodeTypes } from './graph/MGraphNodeTypes';
|
import { TMGraphNodeTypes } from './graph/MGraphNodeTypes';
|
||||||
|
|
||||||
|
const ZOOM_MAX = 2;
|
||||||
|
const ZOOM_MIN = 0.5;
|
||||||
|
|
||||||
interface MGraphFlowProps {
|
interface MGraphFlowProps {
|
||||||
data: TMGraph;
|
data: TMGraph;
|
||||||
}
|
}
|
||||||
|
@ -16,7 +19,6 @@ interface MGraphFlowProps {
|
||||||
function MGraphFlow({ data }: MGraphFlowProps) {
|
function MGraphFlow({ data }: MGraphFlowProps) {
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||||
const [edges, setEdges] = useEdgesState([]);
|
const [edges, setEdges] = useEdgesState([]);
|
||||||
const flow = useReactFlow();
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const newNodes = data.nodes.map(node => ({
|
const newNodes = data.nodes.map(node => ({
|
||||||
|
@ -52,7 +54,7 @@ function MGraphFlow({ data }: MGraphFlowProps) {
|
||||||
|
|
||||||
setNodes(newNodes);
|
setNodes(newNodes);
|
||||||
setEdges(newEdges);
|
setEdges(newEdges);
|
||||||
}, [data, setNodes, setEdges, flow]);
|
}, [data, setNodes, setEdges]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
|
@ -64,8 +66,8 @@ function MGraphFlow({ data }: MGraphFlowProps) {
|
||||||
nodeTypes={TMGraphNodeTypes}
|
nodeTypes={TMGraphNodeTypes}
|
||||||
edgeTypes={TMGraphEdgeTypes}
|
edgeTypes={TMGraphEdgeTypes}
|
||||||
fitView
|
fitView
|
||||||
maxZoom={2}
|
maxZoom={ZOOM_MAX}
|
||||||
minZoom={0.5}
|
minZoom={ZOOM_MIN}
|
||||||
nodesConnectable={false}
|
nodesConnectable={false}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -36,7 +36,12 @@ function MGraphNode(node: MGraphNodeInternal) {
|
||||||
className='w-full h-full cursor-default flex items-center justify-center rounded-full'
|
className='w-full h-full cursor-default flex items-center justify-center rounded-full'
|
||||||
data-tooltip-id={globals.tooltip}
|
data-tooltip-id={globals.tooltip}
|
||||||
data-tooltip-html={tooltipText}
|
data-tooltip-html={tooltipText}
|
||||||
style={{ backgroundColor: colorBgTMGraphNode(node.data, colors) }}
|
style={{
|
||||||
|
backgroundColor: colorBgTMGraphNode(node.data, colors),
|
||||||
|
fontWeight: 600,
|
||||||
|
WebkitTextStrokeWidth: '0.6px',
|
||||||
|
WebkitTextStrokeColor: colors.bgDefault
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{node.data.rank === 0 ? node.data.text : node.data.annotations.length > 0 ? node.data.annotations.length : ''}
|
{node.data.rank === 0 ? node.data.text : node.data.annotations.length > 0 ? node.data.annotations.length : ''}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router';
|
||||||
|
|
||||||
function useQueryStrings() {
|
function useQueryStrings() {
|
||||||
const search = useLocation().search;
|
const search = useLocation().search;
|
||||||
|
|
|
@ -57,11 +57,6 @@ export interface MGraphEdgeInternal extends EdgeProps {
|
||||||
*/
|
*/
|
||||||
export type GraphColoring = 'none' | 'status' | 'type' | 'schemas';
|
export type GraphColoring = 'none' | 'status' | 'type' | 'schemas';
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents graph node sizing scheme.
|
|
||||||
*/
|
|
||||||
export type GraphSizing = 'none' | 'complex' | 'derived';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents manuals topic.
|
* Represents manuals topic.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
*/
|
*/
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
|
|
||||||
import { DependencyMode, GraphSizing, Position2D } from './miscellaneous';
|
import { DependencyMode, Position2D } from './miscellaneous';
|
||||||
import { IOperationPosition, IOperationSchema, OperationID, OperationType } from './oss';
|
import { IOperationPosition, IOperationSchema, OperationID, OperationType } from './oss';
|
||||||
import { IConstituenta, IRSForm } from './rsform';
|
import { IConstituenta, IRSForm } from './rsform';
|
||||||
|
|
||||||
|
@ -38,19 +38,6 @@ export function applyGraphFilter(target: IRSForm, start: number, mode: Dependenc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply {@link GraphSizing} to a given {@link IConstituenta}.
|
|
||||||
*/
|
|
||||||
export function applyNodeSizing(target: IConstituenta, sizing: GraphSizing): number | undefined {
|
|
||||||
if (sizing === 'none') {
|
|
||||||
return undefined;
|
|
||||||
} else if (sizing === 'complex') {
|
|
||||||
return target.is_simple_expression ? 1 : 2;
|
|
||||||
} else {
|
|
||||||
return target.spawner ? 1 : 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate insert position for a new {@link IOperation}
|
* Calculate insert position for a new {@link IOperation}
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
IconImage,
|
IconImage,
|
||||||
IconNewItem,
|
IconNewItem,
|
||||||
IconOSS,
|
IconOSS,
|
||||||
|
IconPredecessor,
|
||||||
IconReset,
|
IconReset,
|
||||||
IconRotate3D,
|
IconRotate3D,
|
||||||
IconText,
|
IconText,
|
||||||
|
@ -32,8 +33,6 @@ function HelpRSGraphTerm() {
|
||||||
<div className='sm:w-[14rem]'>
|
<div className='sm:w-[14rem]'>
|
||||||
<h1>Настройка графа</h1>
|
<h1>Настройка графа</h1>
|
||||||
<li>Цвет – покраска узлов</li>
|
<li>Цвет – покраска узлов</li>
|
||||||
<li>Граф – расположение</li>
|
|
||||||
<li>Размер – размер узлов</li>
|
|
||||||
<li>
|
<li>
|
||||||
<IconText className='inline-icon' /> Отображение текста
|
<IconText className='inline-icon' /> Отображение текста
|
||||||
</li>
|
</li>
|
||||||
|
@ -51,7 +50,7 @@ function HelpRSGraphTerm() {
|
||||||
<h1>Изменение узлов</h1>
|
<h1>Изменение узлов</h1>
|
||||||
<li>Клик на конституенту – выделение</li>
|
<li>Клик на конституенту – выделение</li>
|
||||||
<li>
|
<li>
|
||||||
Ctrl + клик – выбор <span style={{ color: colors.fgPurple }}>фокус-конституенты</span>
|
Alt + клик – выбор <span style={{ color: colors.fgPurple }}>фокус-конституенты</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconReset className='inline-icon' /> Esc – сбросить выделение
|
<IconReset className='inline-icon' /> Esc – сбросить выделение
|
||||||
|
@ -89,9 +88,6 @@ function HelpRSGraphTerm() {
|
||||||
<li>
|
<li>
|
||||||
<IconImage className='inline-icon' /> Сохранить в формат PNG
|
<IconImage className='inline-icon' /> Сохранить в формат PNG
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
* <LinkTopic text='наследованные' topic={HelpTopic.CC_PROPAGATION} /> в ОСС
|
|
||||||
</li>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Divider vertical margins='mx-3' className='hidden sm:block' />
|
<Divider vertical margins='mx-3' className='hidden sm:block' />
|
||||||
|
@ -116,6 +112,10 @@ function HelpRSGraphTerm() {
|
||||||
<li>
|
<li>
|
||||||
<IconGraphCore className='inline-icon' /> выделить <LinkTopic text='Ядро' topic={HelpTopic.CC_SYSTEM} />
|
<IconGraphCore className='inline-icon' /> выделить <LinkTopic text='Ядро' topic={HelpTopic.CC_SYSTEM} />
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<IconPredecessor className='inline-icon' /> выделить{' '}
|
||||||
|
<LinkTopic text='собственные' topic={HelpTopic.CC_PROPAGATION} />
|
||||||
|
</li>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -32,6 +32,9 @@ import { OssNodeTypes } from './graph/OssNodeTypes';
|
||||||
import NodeContextMenu, { ContextMenuData } from './NodeContextMenu';
|
import NodeContextMenu, { ContextMenuData } from './NodeContextMenu';
|
||||||
import ToolbarOssGraph from './ToolbarOssGraph';
|
import ToolbarOssGraph from './ToolbarOssGraph';
|
||||||
|
|
||||||
|
const ZOOM_MAX = 2;
|
||||||
|
const ZOOM_MIN = 0.5;
|
||||||
|
|
||||||
interface OssFlowProps {
|
interface OssFlowProps {
|
||||||
isModified: boolean;
|
isModified: boolean;
|
||||||
setIsModified: (newValue: boolean) => void;
|
setIsModified: (newValue: boolean) => void;
|
||||||
|
@ -223,7 +226,7 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
||||||
const imageWidth = PARAMETER.ossImageWidth;
|
const imageWidth = PARAMETER.ossImageWidth;
|
||||||
const imageHeight = PARAMETER.ossImageHeight;
|
const imageHeight = PARAMETER.ossImageHeight;
|
||||||
const nodesBounds = getNodesBounds(nodes);
|
const nodesBounds = getNodesBounds(nodes);
|
||||||
const viewport = getViewportForBounds(nodesBounds, imageWidth, imageHeight, 0.5, 2);
|
const viewport = getViewportForBounds(nodesBounds, imageWidth, imageHeight, ZOOM_MIN, ZOOM_MAX);
|
||||||
toPng(canvas, {
|
toPng(canvas, {
|
||||||
backgroundColor: colors.bgDefault,
|
backgroundColor: colors.bgDefault,
|
||||||
width: imageWidth,
|
width: imageWidth,
|
||||||
|
@ -266,7 +269,7 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
||||||
setMenuProps(undefined);
|
setMenuProps(undefined);
|
||||||
}, [controller]);
|
}, [controller]);
|
||||||
|
|
||||||
const handleClickCanvas = useCallback(() => {
|
const handleCanvasClick = useCallback(() => {
|
||||||
handleContextMenuHide();
|
handleContextMenuHide();
|
||||||
}, [handleContextMenuHide]);
|
}, [handleContextMenuHide]);
|
||||||
|
|
||||||
|
@ -322,13 +325,13 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
||||||
nodesFocusable={false}
|
nodesFocusable={false}
|
||||||
fitView
|
fitView
|
||||||
nodeTypes={OssNodeTypes}
|
nodeTypes={OssNodeTypes}
|
||||||
maxZoom={2}
|
maxZoom={ZOOM_MAX}
|
||||||
minZoom={0.5}
|
minZoom={ZOOM_MIN}
|
||||||
nodesConnectable={false}
|
nodesConnectable={false}
|
||||||
snapToGrid={true}
|
snapToGrid={true}
|
||||||
snapGrid={[PARAMETER.ossGridSize, PARAMETER.ossGridSize]}
|
snapGrid={[PARAMETER.ossGridSize, PARAMETER.ossGridSize]}
|
||||||
onNodeContextMenu={handleContextMenu}
|
onNodeContextMenu={handleContextMenu}
|
||||||
onClick={handleClickCanvas}
|
onClick={handleCanvasClick}
|
||||||
>
|
>
|
||||||
{showGrid ? <Background gap={PARAMETER.ossGridSize} /> : null}
|
{showGrid ? <Background gap={PARAMETER.ossGridSize} /> : null}
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
|
@ -338,7 +341,7 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
||||||
edges,
|
edges,
|
||||||
handleNodesChange,
|
handleNodesChange,
|
||||||
handleContextMenu,
|
handleContextMenu,
|
||||||
handleClickCanvas,
|
handleCanvasClick,
|
||||||
onEdgesChange,
|
onEdgesChange,
|
||||||
handleNodeDoubleClick,
|
handleNodeDoubleClick,
|
||||||
showGrid
|
showGrid
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router';
|
||||||
|
|
||||||
import { AccessModeState } from '@/context/AccessModeContext';
|
import { AccessModeState } from '@/context/AccessModeContext';
|
||||||
import { OssState } from '@/context/OssContext';
|
import { OssState } from '@/context/OssContext';
|
||||||
|
|
|
@ -51,7 +51,7 @@ function ToolbarConstituenta({
|
||||||
return (
|
return (
|
||||||
<Overlay
|
<Overlay
|
||||||
position='cc-tab-tools right-1/2 translate-x-1/2 xs:right-4 xs:translate-x-0 md:right-1/2 md:translate-x-1/2'
|
position='cc-tab-tools right-1/2 translate-x-1/2 xs:right-4 xs:translate-x-0 md:right-1/2 md:translate-x-1/2'
|
||||||
className='cc-icons outline-none transition-all duration-500'
|
className='cc-icons outline-none transition-all duration-500 cc-blur px-1 rounded-b-2xl'
|
||||||
>
|
>
|
||||||
{controller.schema && controller.schema?.oss.length > 0 ? (
|
{controller.schema && controller.schema?.oss.length > 0 ? (
|
||||||
<MiniSelectorOSS
|
<MiniSelectorOSS
|
||||||
|
|
|
@ -1,390 +1,18 @@
|
||||||
'use client';
|
import { ReactFlowProvider } from 'reactflow';
|
||||||
|
|
||||||
import clsx from 'clsx';
|
import { ConstituentaID } from '@/models/rsform';
|
||||||
import { AnimatePresence } from 'framer-motion';
|
|
||||||
import fileDownload from 'js-file-download';
|
|
||||||
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
||||||
import { useDebounce } from 'use-debounce';
|
|
||||||
|
|
||||||
import InfoConstituenta from '@/components/info/InfoConstituenta';
|
import TGFlow from './TGFlow';
|
||||||
import SelectedCounter from '@/components/info/SelectedCounter';
|
|
||||||
import ToolbarGraphSelection from '@/components/select/ToolbarGraphSelection';
|
|
||||||
import { GraphCanvasRef, GraphEdge, GraphLayout, GraphNode } from '@/components/ui/GraphUI';
|
|
||||||
import Overlay from '@/components/ui/Overlay';
|
|
||||||
import AnimateFade from '@/components/wrap/AnimateFade';
|
|
||||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
|
||||||
import DlgGraphParams from '@/dialogs/DlgGraphParams';
|
|
||||||
import useLocalStorage from '@/hooks/useLocalStorage';
|
|
||||||
import { GraphColoring, GraphFilterParams, GraphSizing } from '@/models/miscellaneous';
|
|
||||||
import { applyNodeSizing } from '@/models/miscellaneousAPI';
|
|
||||||
import { ConstituentaID, CstType, IConstituenta } from '@/models/rsform';
|
|
||||||
import { isBasicConcept } from '@/models/rsformAPI';
|
|
||||||
import { colorBgGraphNode } from '@/styling/color';
|
|
||||||
import { PARAMETER, storage } from '@/utils/constants';
|
|
||||||
import { convertBase64ToBlob } from '@/utils/utils';
|
|
||||||
|
|
||||||
import { useRSEdit } from '../RSEditContext';
|
|
||||||
import GraphSelectors from './GraphSelectors';
|
|
||||||
import TermGraph from './TermGraph';
|
|
||||||
import ToolbarFocusedCst from './ToolbarFocusedCst';
|
|
||||||
import ToolbarTermGraph from './ToolbarTermGraph';
|
|
||||||
import useGraphFilter from './useGraphFilter';
|
|
||||||
import ViewHidden from './ViewHidden';
|
|
||||||
|
|
||||||
interface EditorTermGraphProps {
|
interface EditorTermGraphProps {
|
||||||
onOpenEdit: (cstID: ConstituentaID) => void;
|
onOpenEdit: (cstID: ConstituentaID) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) {
|
function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) {
|
||||||
const controller = useRSEdit();
|
|
||||||
const { colors } = useConceptOptions();
|
|
||||||
|
|
||||||
const [filterParams, setFilterParams] = useLocalStorage<GraphFilterParams>(storage.rsgraphFilter, {
|
|
||||||
noHermits: true,
|
|
||||||
noTemplates: false,
|
|
||||||
noTransitive: true,
|
|
||||||
noText: false,
|
|
||||||
foldDerived: false,
|
|
||||||
|
|
||||||
focusShowInputs: true,
|
|
||||||
focusShowOutputs: true,
|
|
||||||
|
|
||||||
allowBase: true,
|
|
||||||
allowStruct: true,
|
|
||||||
allowTerm: true,
|
|
||||||
allowAxiom: true,
|
|
||||||
allowFunction: true,
|
|
||||||
allowPredicate: true,
|
|
||||||
allowConstant: true,
|
|
||||||
allowTheorem: true
|
|
||||||
});
|
|
||||||
const [showParamsDialog, setShowParamsDialog] = useState(false);
|
|
||||||
const [focusCst, setFocusCst] = useState<IConstituenta | undefined>(undefined);
|
|
||||||
const filtered = useGraphFilter(controller.schema, filterParams, focusCst);
|
|
||||||
|
|
||||||
const graphRef = useRef<GraphCanvasRef | null>(null);
|
|
||||||
const [hidden, setHidden] = useState<ConstituentaID[]>([]);
|
|
||||||
|
|
||||||
const [layout, setLayout] = useLocalStorage<GraphLayout>(storage.rsgraphLayout, 'treeTd2d');
|
|
||||||
const [coloring, setColoring] = useLocalStorage<GraphColoring>(storage.rsgraphColoring, 'type');
|
|
||||||
const [sizing, setSizing] = useLocalStorage<GraphSizing>(storage.rsgraphSizing, 'derived');
|
|
||||||
const [orbit, setOrbit] = useState(false);
|
|
||||||
const is3D = useMemo(() => layout.includes('3d'), [layout]);
|
|
||||||
|
|
||||||
const [hoverID, setHoverID] = useState<ConstituentaID | undefined>(undefined);
|
|
||||||
const hoverCst = useMemo(() => {
|
|
||||||
return hoverID && controller.schema?.cstByID.get(hoverID);
|
|
||||||
}, [controller.schema?.cstByID, hoverID]);
|
|
||||||
const [hoverCstDebounced] = useDebounce(hoverCst, PARAMETER.graphPopupDelay);
|
|
||||||
const [hoverLeft, setHoverLeft] = useState(true);
|
|
||||||
|
|
||||||
const [toggleResetView, setToggleResetView] = useState(false);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
if (!controller.schema) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const newDismissed: ConstituentaID[] = [];
|
|
||||||
controller.schema.items.forEach(cst => {
|
|
||||||
if (!filtered.nodes.has(cst.id)) {
|
|
||||||
newDismissed.push(cst.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setHidden(newDismissed);
|
|
||||||
setHoverID(undefined);
|
|
||||||
}, [controller.schema, filtered]);
|
|
||||||
|
|
||||||
const nodes: GraphNode[] = useMemo(() => {
|
|
||||||
const result: GraphNode[] = [];
|
|
||||||
if (!controller.schema) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
filtered.nodes.forEach(node => {
|
|
||||||
const cst = controller.schema!.cstByID.get(node.id);
|
|
||||||
if (cst) {
|
|
||||||
result.push({
|
|
||||||
id: String(node.id),
|
|
||||||
fill: focusCst === cst ? colors.bgPurple : colorBgGraphNode(cst, coloring, colors),
|
|
||||||
label: `${cst.alias}${cst.is_inherited ? '*' : ''}`,
|
|
||||||
subLabel: !filterParams.noText ? cst.term_resolved : undefined,
|
|
||||||
size: applyNodeSizing(cst, sizing)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}, [controller.schema, coloring, sizing, filtered.nodes, filterParams.noText, colors, focusCst]);
|
|
||||||
|
|
||||||
const edges: GraphEdge[] = useMemo(() => {
|
|
||||||
const result: GraphEdge[] = [];
|
|
||||||
let edgeID = 1;
|
|
||||||
filtered.nodes.forEach(source => {
|
|
||||||
source.outputs.forEach(target => {
|
|
||||||
if (nodes.find(node => node.id === String(target))) {
|
|
||||||
result.push({
|
|
||||||
id: String(edgeID),
|
|
||||||
source: String(source.id),
|
|
||||||
target: String(target)
|
|
||||||
});
|
|
||||||
edgeID += 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}, [filtered.nodes, nodes]);
|
|
||||||
|
|
||||||
function handleCreateCst() {
|
|
||||||
if (!controller.schema) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const definition = controller.selected.map(id => controller.schema!.cstByID.get(id)!.alias).join(' ');
|
|
||||||
controller.createCst(controller.selected.length === 0 ? CstType.BASE : CstType.TERM, false, definition);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDeleteCst() {
|
|
||||||
if (!controller.schema || !controller.canDeleteSelected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
controller.promptDeleteCst();
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleChangeLayout = useCallback(
|
|
||||||
(newLayout: GraphLayout) => {
|
|
||||||
if (newLayout === layout) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setLayout(newLayout);
|
|
||||||
setTimeout(() => {
|
|
||||||
setToggleResetView(prev => !prev);
|
|
||||||
}, PARAMETER.graphRefreshDelay);
|
|
||||||
},
|
|
||||||
[layout, setLayout]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleChangeParams = useCallback(
|
|
||||||
(params: GraphFilterParams) => {
|
|
||||||
setFilterParams(params);
|
|
||||||
},
|
|
||||||
[setFilterParams]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSaveImage = useCallback(() => {
|
|
||||||
if (!graphRef?.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const data = graphRef.current.exportCanvas();
|
|
||||||
try {
|
|
||||||
fileDownload(convertBase64ToBlob(data), 'graph.png', 'data:image/png;base64');
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}, [graphRef]);
|
|
||||||
|
|
||||||
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
|
||||||
if (controller.isProcessing) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (event.key === 'Escape') {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
setFocusCst(undefined);
|
|
||||||
controller.deselectAll();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!controller.isContentEditable) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (event.key === 'Delete') {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
handleDeleteCst();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleFoldDerived = useCallback(() => {
|
|
||||||
setFilterParams(prev => ({
|
|
||||||
...prev,
|
|
||||||
foldDerived: !prev.foldDerived
|
|
||||||
}));
|
|
||||||
setTimeout(() => {
|
|
||||||
setToggleResetView(prev => !prev);
|
|
||||||
}, PARAMETER.graphRefreshDelay);
|
|
||||||
}, [setFilterParams, setToggleResetView]);
|
|
||||||
|
|
||||||
const handleSetFocus = useCallback(
|
|
||||||
(cstID: ConstituentaID | undefined) => {
|
|
||||||
const target = cstID !== undefined ? controller.schema?.cstByID.get(cstID) : cstID;
|
|
||||||
setFocusCst(prev => (prev === target ? undefined : target));
|
|
||||||
if (target) {
|
|
||||||
controller.setSelected([]);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[controller]
|
|
||||||
);
|
|
||||||
|
|
||||||
const graph = useMemo(
|
|
||||||
() => (
|
|
||||||
<TermGraph
|
|
||||||
graphRef={graphRef}
|
|
||||||
nodes={nodes}
|
|
||||||
edges={edges}
|
|
||||||
selectedIDs={controller.selected}
|
|
||||||
layout={layout}
|
|
||||||
is3D={is3D}
|
|
||||||
orbit={orbit}
|
|
||||||
onSelect={controller.select}
|
|
||||||
onDeselect={controller.deselect}
|
|
||||||
setHoverID={setHoverID}
|
|
||||||
onEdit={onOpenEdit}
|
|
||||||
onSelectCentral={handleSetFocus}
|
|
||||||
toggleResetView={toggleResetView}
|
|
||||||
setHoverLeft={setHoverLeft}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
[
|
|
||||||
graphRef,
|
|
||||||
edges,
|
|
||||||
nodes,
|
|
||||||
controller.selected,
|
|
||||||
layout,
|
|
||||||
is3D,
|
|
||||||
orbit,
|
|
||||||
setHoverID,
|
|
||||||
onOpenEdit,
|
|
||||||
toggleResetView,
|
|
||||||
controller.select,
|
|
||||||
controller.deselect,
|
|
||||||
handleSetFocus
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectors = useMemo(
|
|
||||||
() => (
|
|
||||||
<GraphSelectors
|
|
||||||
schema={controller.schema}
|
|
||||||
coloring={coloring}
|
|
||||||
layout={layout}
|
|
||||||
sizing={sizing}
|
|
||||||
setLayout={handleChangeLayout}
|
|
||||||
setColoring={setColoring}
|
|
||||||
setSizing={setSizing}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
[coloring, controller.schema, layout, sizing, handleChangeLayout, setColoring, setSizing]
|
|
||||||
);
|
|
||||||
const viewHidden = useMemo(
|
|
||||||
() => (
|
|
||||||
<ViewHidden
|
|
||||||
items={hidden}
|
|
||||||
selected={controller.selected}
|
|
||||||
schema={controller.schema}
|
|
||||||
coloringScheme={coloring}
|
|
||||||
toggleSelection={controller.toggleSelect}
|
|
||||||
setFocus={handleSetFocus}
|
|
||||||
onEdit={onOpenEdit}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
[hidden, controller.selected, controller.schema, coloring, controller.toggleSelect, handleSetFocus, onOpenEdit]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<ReactFlowProvider>
|
||||||
<AnimatePresence>
|
<TGFlow onOpenEdit={onOpenEdit} />
|
||||||
{showParamsDialog ? (
|
</ReactFlowProvider>
|
||||||
<DlgGraphParams
|
|
||||||
hideWindow={() => setShowParamsDialog(false)}
|
|
||||||
initial={filterParams}
|
|
||||||
onConfirm={handleChangeParams}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</AnimatePresence>
|
|
||||||
|
|
||||||
<Overlay position='cc-tab-tools' className='flex flex-col items-center rounded-b-2xl cc-blur'>
|
|
||||||
<ToolbarTermGraph
|
|
||||||
is3D={is3D}
|
|
||||||
orbit={orbit}
|
|
||||||
noText={filterParams.noText}
|
|
||||||
foldDerived={filterParams.foldDerived}
|
|
||||||
showParamsDialog={() => setShowParamsDialog(true)}
|
|
||||||
onCreate={handleCreateCst}
|
|
||||||
onDelete={handleDeleteCst}
|
|
||||||
onFitView={() => setToggleResetView(prev => !prev)}
|
|
||||||
onSaveImage={handleSaveImage}
|
|
||||||
toggleOrbit={() => setOrbit(prev => !prev)}
|
|
||||||
toggleFoldDerived={handleFoldDerived}
|
|
||||||
toggleNoText={() =>
|
|
||||||
setFilterParams(prev => ({
|
|
||||||
...prev,
|
|
||||||
noText: !prev.noText
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{!focusCst ? (
|
|
||||||
<ToolbarGraphSelection
|
|
||||||
graph={controller.schema!.graph}
|
|
||||||
isCore={cstID => isBasicConcept(controller.schema?.cstByID.get(cstID)?.cst_type)}
|
|
||||||
isOwned={cstID => !controller.schema?.cstByID.get(cstID)?.is_inherited}
|
|
||||||
setSelected={controller.setSelected}
|
|
||||||
emptySelection={controller.selected.length === 0}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
{focusCst ? (
|
|
||||||
<ToolbarFocusedCst
|
|
||||||
center={focusCst}
|
|
||||||
reset={() => handleSetFocus(undefined)}
|
|
||||||
showInputs={filterParams.focusShowInputs}
|
|
||||||
showOutputs={filterParams.focusShowOutputs}
|
|
||||||
toggleShowInputs={() =>
|
|
||||||
setFilterParams(prev => ({
|
|
||||||
...prev,
|
|
||||||
focusShowInputs: !prev.focusShowInputs
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
toggleShowOutputs={() =>
|
|
||||||
setFilterParams(prev => ({
|
|
||||||
...prev,
|
|
||||||
focusShowOutputs: !prev.focusShowOutputs
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</Overlay>
|
|
||||||
|
|
||||||
<AnimateFade tabIndex={-1} onKeyDown={handleKeyDown}>
|
|
||||||
<SelectedCounter
|
|
||||||
hideZero
|
|
||||||
totalCount={controller.schema?.stats?.count_all ?? 0}
|
|
||||||
selectedCount={controller.selected.length}
|
|
||||||
position='top-[4.3rem] sm:top-[2rem] left-0'
|
|
||||||
/>
|
|
||||||
|
|
||||||
{hoverCst && hoverCstDebounced && hoverCst === hoverCstDebounced ? (
|
|
||||||
<Overlay
|
|
||||||
layer='z-tooltip'
|
|
||||||
position={clsx('top-[3.5rem]', { 'left-[2.6rem]': hoverLeft, 'right-[2.6rem]': !hoverLeft })}
|
|
||||||
className={clsx(
|
|
||||||
'w-[25rem] max-h-[calc(100dvh-15rem)]',
|
|
||||||
'px-3',
|
|
||||||
'cc-scroll-y',
|
|
||||||
'border shadow-md',
|
|
||||||
'clr-input',
|
|
||||||
'text-sm'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<InfoConstituenta className='pt-1 pb-2' data={hoverCstDebounced} />
|
|
||||||
</Overlay>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<Overlay position='top-[8.15rem] sm:top-[5.9rem] left-0' className='flex gap-1'>
|
|
||||||
<div className='flex flex-col ml-2 w-[13.5rem]'>
|
|
||||||
{selectors}
|
|
||||||
{viewHidden}
|
|
||||||
</div>
|
|
||||||
</Overlay>
|
|
||||||
|
|
||||||
{graph}
|
|
||||||
</AnimateFade>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,57 +1,34 @@
|
||||||
import BadgeHelp from '@/components/info/BadgeHelp';
|
import BadgeHelp from '@/components/info/BadgeHelp';
|
||||||
import { GraphLayout } from '@/components/ui/GraphUI';
|
|
||||||
import Overlay from '@/components/ui/Overlay';
|
import Overlay from '@/components/ui/Overlay';
|
||||||
import SelectSingle from '@/components/ui/SelectSingle';
|
import SelectSingle from '@/components/ui/SelectSingle';
|
||||||
import { GraphColoring, GraphSizing, HelpTopic } from '@/models/miscellaneous';
|
import { GraphColoring, HelpTopic } from '@/models/miscellaneous';
|
||||||
import { IRSForm } from '@/models/rsform';
|
import { IRSForm } from '@/models/rsform';
|
||||||
import { mapLabelColoring, mapLabelLayout, mapLabelSizing } from '@/utils/labels';
|
import { mapLabelColoring } from '@/utils/labels';
|
||||||
import { SelectorGraphColoring, SelectorGraphLayout, SelectorGraphSizing } from '@/utils/selectors';
|
import { SelectorGraphColoring } from '@/utils/selectors';
|
||||||
|
|
||||||
import SchemasGuide from './SchemasGuide';
|
import SchemasGuide from './SchemasGuide';
|
||||||
|
|
||||||
interface GraphSelectorsProps {
|
interface GraphSelectorsProps {
|
||||||
schema?: IRSForm;
|
schema?: IRSForm;
|
||||||
coloring: GraphColoring;
|
coloring: GraphColoring;
|
||||||
layout: GraphLayout;
|
onChangeColoring: (newValue: GraphColoring) => void;
|
||||||
sizing: GraphSizing;
|
|
||||||
|
|
||||||
setLayout: (newValue: GraphLayout) => void;
|
|
||||||
setColoring: (newValue: GraphColoring) => void;
|
|
||||||
setSizing: (newValue: GraphSizing) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function GraphSelectors({ schema, coloring, setColoring, layout, setLayout, sizing, setSizing }: GraphSelectorsProps) {
|
function GraphSelectors({ schema, coloring, onChangeColoring }: GraphSelectorsProps) {
|
||||||
return (
|
return (
|
||||||
<div className='border rounded-b-none select-none clr-input rounded-t-md'>
|
<div className='border rounded-b-none select-none clr-input rounded-t-md'>
|
||||||
<SelectSingle
|
<Overlay position='right-[2.5rem] top-[0.25rem]'>
|
||||||
noBorder
|
|
||||||
placeholder='Способ расположения'
|
|
||||||
options={SelectorGraphLayout}
|
|
||||||
isSearchable={false}
|
|
||||||
value={layout ? { value: layout, label: mapLabelLayout.get(layout) } : null}
|
|
||||||
onChange={data => setLayout(data?.value ?? SelectorGraphLayout[0].value)}
|
|
||||||
/>
|
|
||||||
<Overlay position='right-[2.5rem] top-[0.5rem]'>
|
|
||||||
{coloring === 'status' ? <BadgeHelp topic={HelpTopic.UI_CST_STATUS} className='min-w-[25rem]' /> : null}
|
{coloring === 'status' ? <BadgeHelp topic={HelpTopic.UI_CST_STATUS} className='min-w-[25rem]' /> : null}
|
||||||
{coloring === 'type' ? <BadgeHelp topic={HelpTopic.UI_CST_CLASS} className='min-w-[25rem]' /> : null}
|
{coloring === 'type' ? <BadgeHelp topic={HelpTopic.UI_CST_CLASS} className='min-w-[25rem]' /> : null}
|
||||||
{coloring === 'schemas' && !!schema ? <SchemasGuide schema={schema} /> : null}
|
{coloring === 'schemas' && !!schema ? <SchemasGuide schema={schema} /> : null}
|
||||||
</Overlay>
|
</Overlay>
|
||||||
<SelectSingle
|
<SelectSingle
|
||||||
className='my-1'
|
|
||||||
noBorder
|
noBorder
|
||||||
placeholder='Цветовая схема'
|
placeholder='Цветовая схема'
|
||||||
options={SelectorGraphColoring}
|
options={SelectorGraphColoring}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
value={coloring ? { value: coloring, label: mapLabelColoring.get(coloring) } : null}
|
value={coloring ? { value: coloring, label: mapLabelColoring.get(coloring) } : null}
|
||||||
onChange={data => setColoring(data?.value ?? SelectorGraphColoring[0].value)}
|
onChange={data => onChangeColoring(data?.value ?? SelectorGraphColoring[0].value)}
|
||||||
/>
|
|
||||||
<SelectSingle
|
|
||||||
noBorder
|
|
||||||
placeholder='Размер узлов'
|
|
||||||
options={SelectorGraphSizing}
|
|
||||||
isSearchable={false}
|
|
||||||
value={layout ? { value: sizing, label: mapLabelSizing.get(sizing) } : null}
|
|
||||||
onChange={data => setSizing(data?.value ?? SelectorGraphSizing[0].value)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,479 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { AnimatePresence } from 'framer-motion';
|
||||||
|
import { toPng } from 'html-to-image';
|
||||||
|
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import {
|
||||||
|
Edge,
|
||||||
|
getNodesBounds,
|
||||||
|
getViewportForBounds,
|
||||||
|
MarkerType,
|
||||||
|
Node,
|
||||||
|
ReactFlow,
|
||||||
|
useEdgesState,
|
||||||
|
useNodesState,
|
||||||
|
useOnSelectionChange,
|
||||||
|
useReactFlow
|
||||||
|
} from 'reactflow';
|
||||||
|
import { useStoreApi } from 'reactflow';
|
||||||
|
import { useDebounce } from 'use-debounce';
|
||||||
|
|
||||||
|
import InfoConstituenta from '@/components/info/InfoConstituenta';
|
||||||
|
import SelectedCounter from '@/components/info/SelectedCounter';
|
||||||
|
import { CProps } from '@/components/props';
|
||||||
|
import ToolbarGraphSelection from '@/components/select/ToolbarGraphSelection';
|
||||||
|
import Overlay from '@/components/ui/Overlay';
|
||||||
|
import AnimateFade from '@/components/wrap/AnimateFade';
|
||||||
|
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||||
|
import DlgGraphParams from '@/dialogs/DlgGraphParams';
|
||||||
|
import useLocalStorage from '@/hooks/useLocalStorage';
|
||||||
|
import { GraphColoring, GraphFilterParams } from '@/models/miscellaneous';
|
||||||
|
import { ConstituentaID, CstType, IConstituenta } from '@/models/rsform';
|
||||||
|
import { isBasicConcept } from '@/models/rsformAPI';
|
||||||
|
import { colorBgGraphNode } from '@/styling/color';
|
||||||
|
import { PARAMETER, storage } from '@/utils/constants';
|
||||||
|
import { errors } from '@/utils/labels';
|
||||||
|
|
||||||
|
import { useRSEdit } from '../RSEditContext';
|
||||||
|
import { TGEdgeTypes } from './graph/TGEdgeTypes';
|
||||||
|
import { applyLayout } from './graph/TGLayout';
|
||||||
|
import { TGNodeData } from './graph/TGNode';
|
||||||
|
import { TGNodeTypes } from './graph/TGNodeTypes';
|
||||||
|
import GraphSelectors from './GraphSelectors';
|
||||||
|
import ToolbarFocusedCst from './ToolbarFocusedCst';
|
||||||
|
import ToolbarTermGraph from './ToolbarTermGraph';
|
||||||
|
import useGraphFilter from './useGraphFilter';
|
||||||
|
import ViewHidden from './ViewHidden';
|
||||||
|
|
||||||
|
const ZOOM_MAX = 3;
|
||||||
|
const ZOOM_MIN = 0.25;
|
||||||
|
|
||||||
|
interface TGFlowProps {
|
||||||
|
onOpenEdit: (cstID: ConstituentaID) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TGFlow({ onOpenEdit }: TGFlowProps) {
|
||||||
|
const { colors, mainHeight } = useConceptOptions();
|
||||||
|
const controller = useRSEdit();
|
||||||
|
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||||
|
const [edges, setEdges] = useEdgesState([]);
|
||||||
|
const flow = useReactFlow();
|
||||||
|
const store = useStoreApi();
|
||||||
|
const { addSelectedNodes } = store.getState();
|
||||||
|
|
||||||
|
const [showParamsDialog, setShowParamsDialog] = useState(false);
|
||||||
|
const [filterParams, setFilterParams] = useLocalStorage<GraphFilterParams>(storage.rsgraphFilter, {
|
||||||
|
noHermits: true,
|
||||||
|
noTemplates: false,
|
||||||
|
noTransitive: true,
|
||||||
|
noText: false,
|
||||||
|
foldDerived: false,
|
||||||
|
|
||||||
|
focusShowInputs: true,
|
||||||
|
focusShowOutputs: true,
|
||||||
|
|
||||||
|
allowBase: true,
|
||||||
|
allowStruct: true,
|
||||||
|
allowTerm: true,
|
||||||
|
allowAxiom: true,
|
||||||
|
allowFunction: true,
|
||||||
|
allowPredicate: true,
|
||||||
|
allowConstant: true,
|
||||||
|
allowTheorem: true
|
||||||
|
});
|
||||||
|
const [coloring, setColoring] = useLocalStorage<GraphColoring>(storage.rsgraphColoring, 'type');
|
||||||
|
|
||||||
|
const [focusCst, setFocusCst] = useState<IConstituenta | undefined>(undefined);
|
||||||
|
const filteredGraph = useGraphFilter(controller.schema, filterParams, focusCst);
|
||||||
|
const [hidden, setHidden] = useState<ConstituentaID[]>([]);
|
||||||
|
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const [hoverID, setHoverID] = useState<ConstituentaID | undefined>(undefined);
|
||||||
|
const hoverCst = useMemo(() => {
|
||||||
|
return hoverID && controller.schema?.cstByID.get(hoverID);
|
||||||
|
}, [controller.schema?.cstByID, hoverID]);
|
||||||
|
const [hoverCstDebounced] = useDebounce(hoverCst, PARAMETER.graphPopupDelay);
|
||||||
|
const [hoverLeft, setHoverLeft] = useState(true);
|
||||||
|
|
||||||
|
const [toggleResetView, setToggleResetView] = useState(false);
|
||||||
|
|
||||||
|
const onSelectionChange = useCallback(
|
||||||
|
({ nodes }: { nodes: Node[] }) => {
|
||||||
|
const ids = nodes.map(node => Number(node.id));
|
||||||
|
if (ids.length === 0) {
|
||||||
|
controller.setSelected([]);
|
||||||
|
} else {
|
||||||
|
controller.setSelected(prev => [...prev.filter(nodeID => !filteredGraph.hasNode(nodeID)), ...ids]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[controller, filteredGraph]
|
||||||
|
);
|
||||||
|
|
||||||
|
useOnSelectionChange({
|
||||||
|
onChange: onSelectionChange
|
||||||
|
});
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!controller.schema) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newDismissed: ConstituentaID[] = [];
|
||||||
|
controller.schema.items.forEach(cst => {
|
||||||
|
if (!filteredGraph.nodes.has(cst.id)) {
|
||||||
|
newDismissed.push(cst.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setHidden(newDismissed);
|
||||||
|
setHoverID(undefined);
|
||||||
|
}, [controller.schema, filteredGraph]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!controller.schema) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newNodes: Node<TGNodeData>[] = [];
|
||||||
|
filteredGraph.nodes.forEach(node => {
|
||||||
|
const cst = controller.schema!.cstByID.get(node.id);
|
||||||
|
if (cst) {
|
||||||
|
newNodes.push({
|
||||||
|
id: String(node.id),
|
||||||
|
type: 'concept',
|
||||||
|
selected: controller.selected.includes(node.id),
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
data: {
|
||||||
|
fill: focusCst === cst ? colors.bgPurple : colorBgGraphNode(cst, coloring, colors),
|
||||||
|
label: cst.alias,
|
||||||
|
description: !filterParams.noText ? cst.term_resolved : ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const newEdges: Edge[] = [];
|
||||||
|
let edgeID = 1;
|
||||||
|
filteredGraph.nodes.forEach(source => {
|
||||||
|
source.outputs.forEach(target => {
|
||||||
|
if (newNodes.find(node => node.id === String(target))) {
|
||||||
|
newEdges.push({
|
||||||
|
id: String(edgeID),
|
||||||
|
source: String(source.id),
|
||||||
|
target: String(target),
|
||||||
|
type: 'termEdge',
|
||||||
|
focusable: false,
|
||||||
|
markerEnd: {
|
||||||
|
type: MarkerType.ArrowClosed,
|
||||||
|
width: 20,
|
||||||
|
height: 20
|
||||||
|
}
|
||||||
|
});
|
||||||
|
edgeID += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
applyLayout(newNodes, newEdges, !filterParams.noText);
|
||||||
|
|
||||||
|
setNodes(newNodes);
|
||||||
|
setEdges(newEdges);
|
||||||
|
// NOTE: Do not rerender on controller.selected change because it is only needed during first load
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [filteredGraph, setNodes, setEdges, controller.schema, filterParams.noText, focusCst, coloring, colors, flow]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
flow.fitView({ duration: PARAMETER.zoomDuration });
|
||||||
|
}, PARAMETER.minimalTimeout);
|
||||||
|
}, [toggleResetView, flow, focusCst, filterParams]);
|
||||||
|
|
||||||
|
function handleSetSelected(newSelection: number[]) {
|
||||||
|
controller.setSelected(newSelection);
|
||||||
|
addSelectedNodes(newSelection.map(id => String(id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCreateCst() {
|
||||||
|
if (!controller.schema) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const definition = controller.selected.map(id => controller.schema!.cstByID.get(id)!.alias).join(' ');
|
||||||
|
controller.createCst(controller.selected.length === 0 ? CstType.BASE : CstType.TERM, false, definition);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeleteCst() {
|
||||||
|
if (!controller.schema || !controller.canDeleteSelected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
controller.promptDeleteCst();
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChangeParams = useCallback(
|
||||||
|
(params: GraphFilterParams) => {
|
||||||
|
setFilterParams(params);
|
||||||
|
},
|
||||||
|
[setFilterParams]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSaveImage = useCallback(() => {
|
||||||
|
if (!controller.schema) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const canvas: HTMLElement | null = document.querySelector('.react-flow__viewport');
|
||||||
|
if (canvas === null) {
|
||||||
|
toast.error(errors.imageFailed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageWidth = PARAMETER.ossImageWidth;
|
||||||
|
const imageHeight = PARAMETER.ossImageHeight;
|
||||||
|
const nodesBounds = getNodesBounds(nodes);
|
||||||
|
const viewport = getViewportForBounds(nodesBounds, imageWidth, imageHeight, ZOOM_MIN, ZOOM_MAX);
|
||||||
|
toPng(canvas, {
|
||||||
|
backgroundColor: colors.bgDefault,
|
||||||
|
width: imageWidth,
|
||||||
|
height: imageHeight,
|
||||||
|
style: {
|
||||||
|
width: String(imageWidth),
|
||||||
|
height: String(imageHeight),
|
||||||
|
transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom * 2})`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(dataURL => {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.setAttribute('download', `${controller.schema?.alias ?? 'graph'}.png`);
|
||||||
|
a.setAttribute('href', dataURL);
|
||||||
|
a.click();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error);
|
||||||
|
toast.error(errors.imageFailed);
|
||||||
|
});
|
||||||
|
}, [colors, nodes, controller.schema]);
|
||||||
|
|
||||||
|
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||||
|
if (controller.isProcessing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
setFocusCst(undefined);
|
||||||
|
handleSetSelected([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!controller.isContentEditable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.key === 'Delete') {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
handleDeleteCst();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFoldDerived = useCallback(() => {
|
||||||
|
setFilterParams(prev => ({
|
||||||
|
...prev,
|
||||||
|
foldDerived: !prev.foldDerived
|
||||||
|
}));
|
||||||
|
setTimeout(() => {
|
||||||
|
setToggleResetView(prev => !prev);
|
||||||
|
}, PARAMETER.graphRefreshDelay);
|
||||||
|
}, [setFilterParams, setToggleResetView]);
|
||||||
|
|
||||||
|
const handleSetFocus = useCallback(
|
||||||
|
(cstID: ConstituentaID | undefined) => {
|
||||||
|
const target = cstID !== undefined ? controller.schema?.cstByID.get(cstID) : cstID;
|
||||||
|
setFocusCst(prev => (prev === target ? undefined : target));
|
||||||
|
if (target) {
|
||||||
|
controller.setSelected([]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[controller]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleNodeClick = useCallback(
|
||||||
|
(event: CProps.EventMouse, cstID: ConstituentaID) => {
|
||||||
|
if (event.altKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
handleSetFocus(cstID);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[handleSetFocus]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleNodeDoubleClick = useCallback(
|
||||||
|
(event: CProps.EventMouse, cstID: ConstituentaID) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
onOpenEdit(cstID);
|
||||||
|
},
|
||||||
|
[onOpenEdit]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleNodeEnter = useCallback(
|
||||||
|
(event: CProps.EventMouse, cstID: ConstituentaID) => {
|
||||||
|
setHoverID(cstID);
|
||||||
|
setHoverLeft(
|
||||||
|
event.clientX / window.innerWidth >= PARAMETER.graphHoverXLimit ||
|
||||||
|
event.clientY / window.innerHeight >= PARAMETER.graphHoverYLimit
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[setHoverID, setHoverLeft]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleNodeLeave = useCallback(() => {
|
||||||
|
setHoverID(undefined);
|
||||||
|
}, [setHoverID]);
|
||||||
|
|
||||||
|
const selectors = useMemo(
|
||||||
|
() => <GraphSelectors schema={controller.schema} coloring={coloring} onChangeColoring={setColoring} />,
|
||||||
|
[coloring, controller.schema, setColoring]
|
||||||
|
);
|
||||||
|
const viewHidden = useMemo(
|
||||||
|
() => (
|
||||||
|
<ViewHidden
|
||||||
|
items={hidden}
|
||||||
|
selected={controller.selected}
|
||||||
|
schema={controller.schema}
|
||||||
|
coloringScheme={coloring}
|
||||||
|
toggleSelection={controller.toggleSelect}
|
||||||
|
setFocus={handleSetFocus}
|
||||||
|
onEdit={onOpenEdit}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[hidden, controller.selected, controller.schema, coloring, controller.toggleSelect, handleSetFocus, onOpenEdit]
|
||||||
|
);
|
||||||
|
|
||||||
|
const graph = useMemo(
|
||||||
|
() => (
|
||||||
|
<div className='relative outline-none w-[100dvw]' style={{ height: mainHeight }}>
|
||||||
|
<ReactFlow
|
||||||
|
nodes={nodes}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
edges={edges}
|
||||||
|
fitView
|
||||||
|
edgesFocusable={false}
|
||||||
|
nodesFocusable={false}
|
||||||
|
nodesConnectable={false}
|
||||||
|
nodeTypes={TGNodeTypes}
|
||||||
|
edgeTypes={TGEdgeTypes}
|
||||||
|
maxZoom={ZOOM_MAX}
|
||||||
|
minZoom={ZOOM_MIN}
|
||||||
|
onNodeDragStart={() => setIsDragging(true)}
|
||||||
|
onNodeDragStop={() => setIsDragging(false)}
|
||||||
|
onNodeMouseEnter={(event, node) => handleNodeEnter(event, Number(node.id))}
|
||||||
|
onNodeMouseLeave={handleNodeLeave}
|
||||||
|
onNodeClick={(event, node) => handleNodeClick(event, Number(node.id))}
|
||||||
|
onNodeDoubleClick={(event, node) => handleNodeDoubleClick(event, Number(node.id))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
[nodes, edges, mainHeight, handleNodeClick, handleNodeDoubleClick, handleNodeLeave, handleNodeEnter, onNodesChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AnimatePresence>
|
||||||
|
{showParamsDialog ? (
|
||||||
|
<DlgGraphParams
|
||||||
|
hideWindow={() => setShowParamsDialog(false)}
|
||||||
|
initial={filterParams}
|
||||||
|
onConfirm={handleChangeParams}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
<AnimateFade tabIndex={-1} onKeyDown={handleKeyDown}>
|
||||||
|
<Overlay position='cc-tab-tools' className='flex flex-col items-center rounded-b-2xl cc-blur'>
|
||||||
|
<ToolbarTermGraph
|
||||||
|
noText={filterParams.noText}
|
||||||
|
foldDerived={filterParams.foldDerived}
|
||||||
|
showParamsDialog={() => setShowParamsDialog(true)}
|
||||||
|
onCreate={handleCreateCst}
|
||||||
|
onDelete={handleDeleteCst}
|
||||||
|
onFitView={() => setToggleResetView(prev => !prev)}
|
||||||
|
onSaveImage={handleSaveImage}
|
||||||
|
toggleFoldDerived={handleFoldDerived}
|
||||||
|
toggleNoText={() =>
|
||||||
|
setFilterParams(prev => ({
|
||||||
|
...prev,
|
||||||
|
noText: !prev.noText
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{!focusCst ? (
|
||||||
|
<ToolbarGraphSelection
|
||||||
|
graph={controller.schema!.graph}
|
||||||
|
isCore={cstID => isBasicConcept(controller.schema?.cstByID.get(cstID)?.cst_type)}
|
||||||
|
isOwned={
|
||||||
|
controller.schema && controller.schema.inheritance.length > 0
|
||||||
|
? cstID => !controller.schema!.cstByID.get(cstID)?.is_inherited
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
selected={controller.selected}
|
||||||
|
setSelected={handleSetSelected}
|
||||||
|
emptySelection={controller.selected.length === 0}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{focusCst ? (
|
||||||
|
<ToolbarFocusedCst
|
||||||
|
center={focusCst}
|
||||||
|
reset={() => handleSetFocus(undefined)}
|
||||||
|
showInputs={filterParams.focusShowInputs}
|
||||||
|
showOutputs={filterParams.focusShowOutputs}
|
||||||
|
toggleShowInputs={() =>
|
||||||
|
setFilterParams(prev => ({
|
||||||
|
...prev,
|
||||||
|
focusShowInputs: !prev.focusShowInputs
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
toggleShowOutputs={() =>
|
||||||
|
setFilterParams(prev => ({
|
||||||
|
...prev,
|
||||||
|
focusShowOutputs: !prev.focusShowOutputs
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</Overlay>
|
||||||
|
|
||||||
|
<SelectedCounter
|
||||||
|
hideZero
|
||||||
|
totalCount={controller.schema?.stats?.count_all ?? 0}
|
||||||
|
selectedCount={controller.selected.length}
|
||||||
|
position='top-[4.4rem] sm:top-[4.1rem] left-[0.5rem] sm:left-[0.65rem]'
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!isDragging && hoverCst && hoverCstDebounced && hoverCst === hoverCstDebounced ? (
|
||||||
|
<Overlay
|
||||||
|
layer='z-tooltip'
|
||||||
|
position={clsx('top-[3.5rem]', { 'left-[2.6rem]': hoverLeft, 'right-[2.6rem]': !hoverLeft })}
|
||||||
|
className={clsx(
|
||||||
|
'w-[25rem] max-h-[calc(100dvh-15rem)]',
|
||||||
|
'px-3',
|
||||||
|
'cc-scroll-y',
|
||||||
|
'border shadow-md',
|
||||||
|
'clr-input',
|
||||||
|
'text-sm'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<InfoConstituenta className='pt-1 pb-2' data={hoverCstDebounced} />
|
||||||
|
</Overlay>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Overlay position='top-[6.15rem] sm:top-[5.9rem] left-0' className='flex gap-1'>
|
||||||
|
<div className='flex flex-col ml-2 w-[13.5rem]'>
|
||||||
|
{selectors}
|
||||||
|
{viewHidden}
|
||||||
|
</div>
|
||||||
|
</Overlay>
|
||||||
|
{graph}
|
||||||
|
</AnimateFade>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TGFlow;
|
|
@ -1,137 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { RefObject, useCallback, useLayoutEffect } from 'react';
|
|
||||||
|
|
||||||
import GraphUI, {
|
|
||||||
CollapseProps,
|
|
||||||
GraphCanvasRef,
|
|
||||||
GraphEdge,
|
|
||||||
GraphLayout,
|
|
||||||
GraphMouseEvent,
|
|
||||||
GraphNode,
|
|
||||||
GraphPointerEvent,
|
|
||||||
useSelection
|
|
||||||
} from '@/components/ui/GraphUI';
|
|
||||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
|
||||||
import { ConstituentaID } from '@/models/rsform';
|
|
||||||
import { graphDarkT, graphLightT } from '@/styling/color';
|
|
||||||
import { PARAMETER, resources } from '@/utils/constants';
|
|
||||||
|
|
||||||
interface TermGraphProps {
|
|
||||||
graphRef: RefObject<GraphCanvasRef>;
|
|
||||||
nodes: GraphNode[];
|
|
||||||
edges: GraphEdge[];
|
|
||||||
selectedIDs: ConstituentaID[];
|
|
||||||
|
|
||||||
layout: GraphLayout;
|
|
||||||
is3D: boolean;
|
|
||||||
orbit: boolean;
|
|
||||||
|
|
||||||
setHoverID: (newID: ConstituentaID | undefined) => void;
|
|
||||||
setHoverLeft: (value: boolean) => void;
|
|
||||||
onEdit: (cstID: ConstituentaID) => void;
|
|
||||||
onSelectCentral: (selectedID: ConstituentaID) => void;
|
|
||||||
onSelect: (newID: ConstituentaID) => void;
|
|
||||||
onDeselect: (newID: ConstituentaID) => void;
|
|
||||||
|
|
||||||
toggleResetView: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function TermGraph({
|
|
||||||
graphRef,
|
|
||||||
nodes,
|
|
||||||
edges,
|
|
||||||
selectedIDs,
|
|
||||||
layout,
|
|
||||||
is3D,
|
|
||||||
orbit,
|
|
||||||
toggleResetView,
|
|
||||||
setHoverID,
|
|
||||||
setHoverLeft,
|
|
||||||
onEdit,
|
|
||||||
onSelectCentral,
|
|
||||||
onSelect,
|
|
||||||
onDeselect
|
|
||||||
}: TermGraphProps) {
|
|
||||||
const { mainHeight, darkMode } = useConceptOptions();
|
|
||||||
|
|
||||||
const { selections, setSelections } = useSelection({
|
|
||||||
ref: graphRef,
|
|
||||||
nodes: nodes,
|
|
||||||
edges: edges,
|
|
||||||
type: 'multi',
|
|
||||||
focusOnSelect: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleHoverIn = useCallback(
|
|
||||||
(node: GraphNode, event: GraphPointerEvent) => {
|
|
||||||
setHoverID(Number(node.id));
|
|
||||||
setHoverLeft(
|
|
||||||
event.clientX / window.innerWidth >= PARAMETER.graphHoverXLimit ||
|
|
||||||
event.clientY / window.innerHeight >= PARAMETER.graphHoverYLimit
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[setHoverID, setHoverLeft]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleHoverOut = useCallback(() => {
|
|
||||||
setHoverID(undefined);
|
|
||||||
}, [setHoverID]);
|
|
||||||
|
|
||||||
const handleNodeClick = useCallback(
|
|
||||||
(node: GraphNode, _?: CollapseProps, event?: GraphMouseEvent) => {
|
|
||||||
if (event?.ctrlKey || event?.metaKey) {
|
|
||||||
onSelectCentral(Number(node.id));
|
|
||||||
} else if (selections.includes(node.id)) {
|
|
||||||
onDeselect(Number(node.id));
|
|
||||||
} else {
|
|
||||||
onSelect(Number(node.id));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[onSelect, selections, onDeselect, onSelectCentral]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleNodeDoubleClick = useCallback(
|
|
||||||
(node: GraphNode) => {
|
|
||||||
onEdit(Number(node.id));
|
|
||||||
},
|
|
||||||
[onEdit]
|
|
||||||
);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
graphRef.current?.fitNodesInView([], { animated: true });
|
|
||||||
}, [toggleResetView, graphRef]);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
const newSelections = nodes.filter(node => selectedIDs.includes(Number(node.id))).map(node => node.id);
|
|
||||||
setSelections(newSelections);
|
|
||||||
}, [selectedIDs, setSelections, nodes]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='relative outline-none w-[100dvw]' style={{ height: mainHeight }}>
|
|
||||||
<GraphUI
|
|
||||||
nodes={nodes}
|
|
||||||
edges={edges}
|
|
||||||
ref={graphRef}
|
|
||||||
animated={false}
|
|
||||||
draggable
|
|
||||||
layoutType={layout}
|
|
||||||
selections={selections}
|
|
||||||
onNodeDoubleClick={handleNodeDoubleClick}
|
|
||||||
onNodeClick={handleNodeClick}
|
|
||||||
onNodePointerOver={handleHoverIn}
|
|
||||||
onNodePointerOut={handleHoverOut}
|
|
||||||
minNodeSize={4}
|
|
||||||
maxNodeSize={8}
|
|
||||||
cameraMode={orbit ? 'orbit' : is3D ? 'rotate' : 'pan'}
|
|
||||||
layoutOverrides={
|
|
||||||
layout.includes('tree') ? { nodeLevelRatio: nodes.length < PARAMETER.smallTreeNodes ? 3 : 1 } : undefined
|
|
||||||
}
|
|
||||||
labelFontUrl={resources.graph_font}
|
|
||||||
theme={darkMode ? graphDarkT : graphLightT}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TermGraph;
|
|
|
@ -8,7 +8,6 @@ import {
|
||||||
IconFitImage,
|
IconFitImage,
|
||||||
IconImage,
|
IconImage,
|
||||||
IconNewItem,
|
IconNewItem,
|
||||||
IconRotate3D,
|
|
||||||
IconText,
|
IconText,
|
||||||
IconTextOff,
|
IconTextOff,
|
||||||
IconTypeGraph
|
IconTypeGraph
|
||||||
|
@ -22,9 +21,6 @@ import { PARAMETER } from '@/utils/constants';
|
||||||
import { useRSEdit } from '../RSEditContext';
|
import { useRSEdit } from '../RSEditContext';
|
||||||
|
|
||||||
interface ToolbarTermGraphProps {
|
interface ToolbarTermGraphProps {
|
||||||
is3D: boolean;
|
|
||||||
|
|
||||||
orbit: boolean;
|
|
||||||
noText: boolean;
|
noText: boolean;
|
||||||
foldDerived: boolean;
|
foldDerived: boolean;
|
||||||
|
|
||||||
|
@ -36,17 +32,13 @@ interface ToolbarTermGraphProps {
|
||||||
|
|
||||||
toggleFoldDerived: () => void;
|
toggleFoldDerived: () => void;
|
||||||
toggleNoText: () => void;
|
toggleNoText: () => void;
|
||||||
toggleOrbit: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ToolbarTermGraph({
|
function ToolbarTermGraph({
|
||||||
is3D,
|
|
||||||
noText,
|
noText,
|
||||||
foldDerived,
|
foldDerived,
|
||||||
toggleNoText,
|
toggleNoText,
|
||||||
toggleFoldDerived,
|
toggleFoldDerived,
|
||||||
orbit,
|
|
||||||
toggleOrbit,
|
|
||||||
showParamsDialog,
|
showParamsDialog,
|
||||||
onCreate,
|
onCreate,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
@ -95,12 +87,6 @@ function ToolbarTermGraph({
|
||||||
}
|
}
|
||||||
onClick={toggleFoldDerived}
|
onClick={toggleFoldDerived}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
|
||||||
icon={<IconRotate3D size='1.25rem' className={orbit ? 'icon-green' : 'icon-primary'} />}
|
|
||||||
title='Анимация вращения'
|
|
||||||
disabled={!is3D}
|
|
||||||
onClick={toggleOrbit}
|
|
||||||
/>
|
|
||||||
{controller.isContentEditable ? (
|
{controller.isContentEditable ? (
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Новая конституента'
|
title='Новая конституента'
|
||||||
|
|
|
@ -76,7 +76,9 @@ function ViewHidden({ items, selected, toggleSelection, setFocus, schema, colori
|
||||||
animate={!isFolded ? 'open' : 'closed'}
|
animate={!isFolded ? 'open' : 'closed'}
|
||||||
variants={animateHiddenHeader}
|
variants={animateHiddenHeader}
|
||||||
initial={false}
|
initial={false}
|
||||||
>{`Скрытые [${localSelected.length} | ${items.length}]`}</motion.div>
|
>
|
||||||
|
{`Скрытые [${localSelected.length} | ${items.length}]`}
|
||||||
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
|
@ -87,7 +89,7 @@ function ViewHidden({ items, selected, toggleSelection, setFocus, schema, colori
|
||||||
'text-sm',
|
'text-sm',
|
||||||
'cc-scroll-y'
|
'cc-scroll-y'
|
||||||
)}
|
)}
|
||||||
style={{ maxHeight: calculateHeight(windowSize.isSmall ? '12.rem + 2px' : '16.4rem + 2px') }}
|
style={{ maxHeight: calculateHeight(windowSize.isSmall ? '10.4rem + 2px' : '12.5rem + 2px') }}
|
||||||
initial={false}
|
initial={false}
|
||||||
animate={!isFolded ? 'open' : 'closed'}
|
animate={!isFolded ? 'open' : 'closed'}
|
||||||
variants={animateDropdown}
|
variants={animateDropdown}
|
||||||
|
@ -117,7 +119,6 @@ function ViewHidden({ items, selected, toggleSelection, setFocus, schema, colori
|
||||||
onDoubleClick={() => onEdit(cstID)}
|
onDoubleClick={() => onEdit(cstID)}
|
||||||
>
|
>
|
||||||
{cst.alias}
|
{cst.alias}
|
||||||
{cst.is_inherited ? '*' : ''}
|
|
||||||
</button>
|
</button>
|
||||||
<TooltipConstituenta data={cst} anchor={`#${id}`} />
|
<TooltipConstituenta data={cst} anchor={`#${id}`} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { EdgeTypes } from 'reactflow';
|
||||||
|
|
||||||
|
import DynamicEdge from '../../../../components/ui/Flow/DynamicEdge';
|
||||||
|
|
||||||
|
export const TGEdgeTypes: EdgeTypes = {
|
||||||
|
termEdge: DynamicEdge
|
||||||
|
};
|
|
@ -0,0 +1,32 @@
|
||||||
|
import dagre from '@dagrejs/dagre';
|
||||||
|
import { Edge, Node } from 'reactflow';
|
||||||
|
|
||||||
|
import { PARAMETER } from '@/utils/constants';
|
||||||
|
|
||||||
|
import { TGNodeData } from './TGNode';
|
||||||
|
|
||||||
|
export function applyLayout(nodes: Node<TGNodeData>[], edges: Edge[], subLabels?: boolean) {
|
||||||
|
const dagreGraph = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
||||||
|
dagreGraph.setGraph({
|
||||||
|
rankdir: 'TB',
|
||||||
|
ranksep: subLabels ? 60 : 40,
|
||||||
|
nodesep: subLabels ? 100 : 20,
|
||||||
|
ranker: 'network-simplex',
|
||||||
|
align: undefined
|
||||||
|
});
|
||||||
|
nodes.forEach(node => {
|
||||||
|
dagreGraph.setNode(node.id, { width: 2 * PARAMETER.graphNodeRadius, height: 2 * PARAMETER.graphNodeRadius });
|
||||||
|
});
|
||||||
|
|
||||||
|
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 - PARAMETER.graphNodeRadius;
|
||||||
|
node.position.y = nodeWithPosition.y - PARAMETER.graphNodeRadius;
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { Handle, Position } from 'reactflow';
|
||||||
|
|
||||||
|
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||||
|
import { truncateToLastWord } from '@/utils/utils';
|
||||||
|
|
||||||
|
const MAX_DESCRIPTION_LENGTH = 65;
|
||||||
|
const DESCRIPTION_THRESHOLD = 15;
|
||||||
|
const LABEL_THRESHOLD = 3;
|
||||||
|
|
||||||
|
const FONT_SIZE_MAX = 14;
|
||||||
|
const FONT_SIZE_MED = 12;
|
||||||
|
const FONT_SIZE_MIN = 10;
|
||||||
|
|
||||||
|
export interface TGNodeData {
|
||||||
|
fill: string;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents graph AST node internal data.
|
||||||
|
*/
|
||||||
|
interface TGNodeInternal {
|
||||||
|
id: string;
|
||||||
|
data: TGNodeData;
|
||||||
|
selected: boolean;
|
||||||
|
dragging: boolean;
|
||||||
|
xPos: number;
|
||||||
|
yPos: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TGNode(node: TGNodeInternal) {
|
||||||
|
const { colors } = useConceptOptions();
|
||||||
|
const description = useMemo(
|
||||||
|
() => truncateToLastWord(node.data.description, MAX_DESCRIPTION_LENGTH),
|
||||||
|
[node.data.description]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Handle type='target' position={Position.Top} style={{ opacity: 0 }} />
|
||||||
|
<div
|
||||||
|
className='w-full h-full cursor-default flex items-center justify-center rounded-full'
|
||||||
|
style={{
|
||||||
|
backgroundColor: !node.selected ? node.data.fill : colors.bgActiveSelection,
|
||||||
|
fontSize: node.data.label.length > LABEL_THRESHOLD ? FONT_SIZE_MED : FONT_SIZE_MAX
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontWeight: 600,
|
||||||
|
WebkitTextStrokeWidth: '0.6px',
|
||||||
|
WebkitTextStrokeColor: colors.bgDefault
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{node.data.label}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Handle type='source' position={Position.Bottom} style={{ opacity: 0 }} />
|
||||||
|
{description ? (
|
||||||
|
<div
|
||||||
|
className='mt-1 w-[150px] px-1 text-center translate-x-[calc(-50%+20px)]'
|
||||||
|
style={{
|
||||||
|
fontSize: description.length > DESCRIPTION_THRESHOLD ? FONT_SIZE_MIN : FONT_SIZE_MED
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className='absolute top-0 px-1 left-0 text-center w-full'>{description}</div>
|
||||||
|
<div
|
||||||
|
aria-hidden='true'
|
||||||
|
style={{
|
||||||
|
WebkitTextStrokeWidth: '3px',
|
||||||
|
WebkitTextStrokeColor: colors.bgDefault
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TGNode;
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { NodeTypes } from 'reactflow';
|
||||||
|
|
||||||
|
import TGNode from './TGNode';
|
||||||
|
|
||||||
|
export const TGNodeTypes: NodeTypes = {
|
||||||
|
concept: TGNode
|
||||||
|
};
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router';
|
||||||
|
|
||||||
import { AccessModeState } from '@/context/AccessModeContext';
|
import { AccessModeState } from '@/context/AccessModeContext';
|
||||||
import { RSFormState } from '@/context/RSFormContext';
|
import { RSFormState } from '@/context/RSFormContext';
|
||||||
|
|
|
@ -20,6 +20,7 @@ export interface IColorTheme {
|
||||||
bgDisabled: string;
|
bgDisabled: string;
|
||||||
bgPrimary: string;
|
bgPrimary: string;
|
||||||
bgSelected: string;
|
bgSelected: string;
|
||||||
|
bgActiveSelection: string;
|
||||||
bgHover: string;
|
bgHover: string;
|
||||||
bgWarning: string;
|
bgWarning: string;
|
||||||
|
|
||||||
|
@ -62,6 +63,7 @@ export const lightT: IColorTheme = {
|
||||||
bgDisabled: 'var(--cl-bg-60)',
|
bgDisabled: 'var(--cl-bg-60)',
|
||||||
bgPrimary: 'var(--cl-prim-bg-100)',
|
bgPrimary: 'var(--cl-prim-bg-100)',
|
||||||
bgSelected: 'var(--cl-prim-bg-80)',
|
bgSelected: 'var(--cl-prim-bg-80)',
|
||||||
|
bgActiveSelection: 'var(--cl-teal-bg-100)',
|
||||||
bgHover: 'var(--cl-prim-bg-60)',
|
bgHover: 'var(--cl-prim-bg-60)',
|
||||||
bgWarning: 'var(--cl-red-bg-100)',
|
bgWarning: 'var(--cl-red-bg-100)',
|
||||||
|
|
||||||
|
@ -104,6 +106,7 @@ export const darkT: IColorTheme = {
|
||||||
bgDisabled: 'var(--cd-bg-60)',
|
bgDisabled: 'var(--cd-bg-60)',
|
||||||
bgPrimary: 'var(--cd-prim-bg-100)',
|
bgPrimary: 'var(--cd-prim-bg-100)',
|
||||||
bgSelected: 'var(--cd-prim-bg-80)',
|
bgSelected: 'var(--cd-prim-bg-80)',
|
||||||
|
bgActiveSelection: 'var(--cd-teal-bg-100)',
|
||||||
bgHover: 'var(--cd-prim-bg-60)',
|
bgHover: 'var(--cd-prim-bg-60)',
|
||||||
bgWarning: 'var(--cd-red-bg-100)',
|
bgWarning: 'var(--cd-red-bg-100)',
|
||||||
|
|
||||||
|
@ -184,96 +187,6 @@ export const selectDarkT = {
|
||||||
neutral90: darkT.fgWarning
|
neutral90: darkT.fgWarning
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents Graph component Light theme.
|
|
||||||
*/
|
|
||||||
export const graphLightT = {
|
|
||||||
canvas: {
|
|
||||||
background: '#f9fafb'
|
|
||||||
},
|
|
||||||
node: {
|
|
||||||
fill: '#7ca0ab',
|
|
||||||
activeFill: '#1DE9AC',
|
|
||||||
opacity: 1,
|
|
||||||
selectedOpacity: 1,
|
|
||||||
inactiveOpacity: 1,
|
|
||||||
label: {
|
|
||||||
color: '#2A6475',
|
|
||||||
stroke: '#fff',
|
|
||||||
activeColor: '#1DE9AC'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
lasso: {
|
|
||||||
border: '1px solid #55aaff',
|
|
||||||
background: 'rgba(75, 160, 255, 0.1)'
|
|
||||||
},
|
|
||||||
ring: {
|
|
||||||
fill: '#D8E6EA',
|
|
||||||
activeFill: '#1DE9AC'
|
|
||||||
},
|
|
||||||
edge: {
|
|
||||||
fill: '#D8E6EA',
|
|
||||||
activeFill: '#1DE9AC',
|
|
||||||
opacity: 1,
|
|
||||||
selectedOpacity: 1,
|
|
||||||
inactiveOpacity: 1,
|
|
||||||
label: {
|
|
||||||
stroke: '#fff',
|
|
||||||
color: '#2A6475',
|
|
||||||
activeColor: '#1DE9AC'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
arrow: {
|
|
||||||
fill: '#D8E6EA',
|
|
||||||
activeFill: '#1DE9AC'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents Graph component Dark theme.
|
|
||||||
*/
|
|
||||||
export const graphDarkT = {
|
|
||||||
canvas: {
|
|
||||||
background: '#171717' // var(--cd-bg-100)
|
|
||||||
},
|
|
||||||
node: {
|
|
||||||
fill: '#7a8c9e',
|
|
||||||
activeFill: '#1DE9AC',
|
|
||||||
opacity: 1,
|
|
||||||
selectedOpacity: 1,
|
|
||||||
inactiveOpacity: 1,
|
|
||||||
label: {
|
|
||||||
stroke: '#1E2026',
|
|
||||||
color: '#ACBAC7',
|
|
||||||
activeColor: '#1DE9AC'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
lasso: {
|
|
||||||
border: '1px solid #55aaff',
|
|
||||||
background: 'rgba(75, 160, 255, 0.1)'
|
|
||||||
},
|
|
||||||
ring: {
|
|
||||||
fill: '#54616D',
|
|
||||||
activeFill: '#1DE9AC'
|
|
||||||
},
|
|
||||||
edge: {
|
|
||||||
fill: '#474B56',
|
|
||||||
activeFill: '#1DE9AC',
|
|
||||||
opacity: 1,
|
|
||||||
selectedOpacity: 1,
|
|
||||||
inactiveOpacity: 1,
|
|
||||||
label: {
|
|
||||||
stroke: '#1E2026',
|
|
||||||
color: '#ACBAC7',
|
|
||||||
activeColor: '#1DE9AC'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
arrow: {
|
|
||||||
fill: '#474B56',
|
|
||||||
activeFill: '#1DE9AC'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents Brackets highlights Light theme.
|
* Represents Brackets highlights Light theme.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -38,6 +38,8 @@
|
||||||
--cl-red-fg-100: hsl(000, 072%, 051%);
|
--cl-red-fg-100: hsl(000, 072%, 051%);
|
||||||
--cl-green-fg-100: hsl(120, 080%, 37%);
|
--cl-green-fg-100: hsl(120, 080%, 37%);
|
||||||
|
|
||||||
|
--cl-teal-bg-100: hsl(162, 082%, 051%);
|
||||||
|
|
||||||
/* Dark Theme */
|
/* Dark Theme */
|
||||||
--cd-bg-120: hsl(000, 000%, 005%);
|
--cd-bg-120: hsl(000, 000%, 005%);
|
||||||
--cd-bg-100: hsl(000, 000%, 009%);
|
--cd-bg-100: hsl(000, 000%, 009%);
|
||||||
|
@ -59,4 +61,6 @@
|
||||||
--cd-red-bg-100: hsl(000, 100%, 015%);
|
--cd-red-bg-100: hsl(000, 100%, 015%);
|
||||||
--cd-red-fg-100: hsl(000, 080%, 055%);
|
--cd-red-fg-100: hsl(000, 080%, 055%);
|
||||||
--cd-green-fg-100: hsl(120, 080%, 042%);
|
--cd-green-fg-100: hsl(120, 080%, 042%);
|
||||||
|
|
||||||
|
--cd-teal-bg-100: hsl(162, 082%, 041%);
|
||||||
}
|
}
|
||||||
|
|
|
@ -97,10 +97,6 @@
|
||||||
box-shadow: 0 0 0 2px var(--cl-prim-bg-80) !important;
|
box-shadow: 0 0 0 2px var(--cl-prim-bg-80) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.selected {
|
|
||||||
border-color: var(--cd-bg-40);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark & {
|
.dark & {
|
||||||
color: var(--cd-fg-100);
|
color: var(--cd-fg-100);
|
||||||
border-color: var(--cd-bg-40);
|
border-color: var(--cd-bg-40);
|
||||||
|
@ -109,10 +105,6 @@
|
||||||
&:hover:not(.selected) {
|
&:hover:not(.selected) {
|
||||||
box-shadow: 0 0 0 3px var(--cd-prim-bg-80) !important;
|
box-shadow: 0 0 0 3px var(--cd-prim-bg-80) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.selected {
|
|
||||||
border-color: var(--cl-bg-40);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,13 +116,39 @@
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
width: 150px;
|
width: 150px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
border-color: var(--cd-bg-40);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
&.selected {
|
||||||
|
border-color: var(--cl-bg-40);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-flow__node-step,
|
.react-flow__node-step,
|
||||||
.react-flow__node-token {
|
.react-flow__node-token,
|
||||||
|
.react-flow__node-concept {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
|
|
||||||
|
outline-offset: 4px;
|
||||||
|
outline-style: solid;
|
||||||
|
outline-color: transparent;
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
outline-color: var(--cl-teal-bg-100);
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
&.selected {
|
||||||
|
border-color: var(--cd-teal-bg-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -206,7 +206,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.cc-tab-tools {
|
.cc-tab-tools {
|
||||||
@apply top-[1.9rem] pt-1 right-1/2 translate-x-1/2;
|
@apply top-[1.7rem] pt-[0.4rem] pb-1 right-1/2 translate-x-1/2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cc-label {
|
.cc-label {
|
||||||
|
|
|
@ -10,8 +10,8 @@ export const PARAMETER = {
|
||||||
smallTreeNodes: 50, // amount of nodes threshold for size increase for large graphs
|
smallTreeNodes: 50, // amount of nodes threshold for size increase for large graphs
|
||||||
refreshTimeout: 100, // milliseconds delay for post-refresh actions
|
refreshTimeout: 100, // milliseconds delay for post-refresh actions
|
||||||
minimalTimeout: 10, // milliseconds delay for fast updates
|
minimalTimeout: 10, // milliseconds delay for fast updates
|
||||||
|
|
||||||
zoomDuration: 500, // milliseconds animation duration
|
zoomDuration: 500, // milliseconds animation duration
|
||||||
|
|
||||||
ossImageWidth: 1280, // pixels - size of OSS image
|
ossImageWidth: 1280, // pixels - size of OSS image
|
||||||
ossImageHeight: 960, // pixels - size of OSS image
|
ossImageHeight: 960, // pixels - size of OSS image
|
||||||
ossContextMenuWidth: 200, // pixels - width of OSS context menu
|
ossContextMenuWidth: 200, // pixels - width of OSS context menu
|
||||||
|
@ -21,16 +21,17 @@ export const PARAMETER = {
|
||||||
ossDistanceX: 180, // pixels - insert x-distance between node centers
|
ossDistanceX: 180, // pixels - insert x-distance between node centers
|
||||||
ossDistanceY: 100, // pixels - insert y-distance between node centers
|
ossDistanceY: 100, // pixels - insert y-distance between node centers
|
||||||
|
|
||||||
|
graphHandleSize: 3, // pixels - size of graph connection handle
|
||||||
|
graphNodeRadius: 20, // pixels - radius of graph node
|
||||||
|
graphNodePadding: 5, // pixels - padding of graph node
|
||||||
graphHoverXLimit: 0.4, // ratio to clientWidth used to determine which side of screen popup should be
|
graphHoverXLimit: 0.4, // ratio to clientWidth used to determine which side of screen popup should be
|
||||||
graphHoverYLimit: 0.6, // ratio to clientHeight used to determine which side of screen popup should be
|
graphHoverYLimit: 0.6, // ratio to clientHeight used to determine which side of screen popup should be
|
||||||
graphPopupDelay: 500, // milliseconds delay for graph popup selections
|
graphPopupDelay: 500, // milliseconds delay for graph popup selections
|
||||||
graphRefreshDelay: 10, // milliseconds delay for graph viewpoint reset
|
graphRefreshDelay: 10, // milliseconds delay for graph viewpoint reset
|
||||||
|
|
||||||
typificationTruncate: 42, // characters - threshold for long typification - truncate
|
typificationTruncate: 42, // characters - threshold for long typification - truncate
|
||||||
|
|
||||||
ossLongLabel: 14, // characters - threshold for long labels - small font
|
ossLongLabel: 14, // characters - threshold for long labels - small font
|
||||||
ossTruncateLabel: 32, // characters - threshold for long labels - truncate
|
ossTruncateLabel: 32, // characters - threshold for long labels - truncate
|
||||||
|
|
||||||
statSmallThreshold: 3, // characters - threshold for small labels - small font
|
statSmallThreshold: 3, // characters - threshold for small labels - small font
|
||||||
|
|
||||||
logicLabel: 'LOGIC',
|
logicLabel: 'LOGIC',
|
||||||
|
@ -64,7 +65,6 @@ export const patterns = {
|
||||||
* Local URIs.
|
* Local URIs.
|
||||||
*/
|
*/
|
||||||
export const resources = {
|
export const resources = {
|
||||||
graph_font: '/DejaVu.ttf',
|
|
||||||
privacy_policy: '/privacy.pdf',
|
privacy_policy: '/privacy.pdf',
|
||||||
logo: '/logo_full.svg',
|
logo: '/logo_full.svg',
|
||||||
db_schema: '/db_schema.svg'
|
db_schema: '/db_schema.svg'
|
||||||
|
@ -121,9 +121,7 @@ export const storage = {
|
||||||
libraryPagination: 'library.pagination',
|
libraryPagination: 'library.pagination',
|
||||||
|
|
||||||
rsgraphFilter: 'rsgraph.filter2',
|
rsgraphFilter: 'rsgraph.filter2',
|
||||||
rsgraphLayout: 'rsgraph.layout',
|
|
||||||
rsgraphColoring: 'rsgraph.coloring',
|
rsgraphColoring: 'rsgraph.coloring',
|
||||||
rsgraphSizing: 'rsgraph.sizing',
|
|
||||||
rsgraphFoldHidden: 'rsgraph.fold_hidden',
|
rsgraphFoldHidden: 'rsgraph.fold_hidden',
|
||||||
|
|
||||||
ossShowGrid: 'oss.show_grid',
|
ossShowGrid: 'oss.show_grid',
|
||||||
|
|
|
@ -4,12 +4,11 @@
|
||||||
* Label is a short text used to represent an entity.
|
* Label is a short text used to represent an entity.
|
||||||
* Description is a long description used in tooltips.
|
* Description is a long description used in tooltips.
|
||||||
*/
|
*/
|
||||||
import { GraphLayout } from '@/components/ui/GraphUI';
|
|
||||||
import { FolderNode } from '@/models/FolderTree';
|
import { FolderNode } from '@/models/FolderTree';
|
||||||
import { GramData, Grammeme, ReferenceType } from '@/models/language';
|
import { GramData, Grammeme, ReferenceType } from '@/models/language';
|
||||||
import { AccessPolicy, LibraryItemType, LocationHead } from '@/models/library';
|
import { AccessPolicy, LibraryItemType, LocationHead } from '@/models/library';
|
||||||
import { validateLocation } from '@/models/libraryAPI';
|
import { validateLocation } from '@/models/libraryAPI';
|
||||||
import { CstMatchMode, DependencyMode, GraphColoring, GraphSizing, HelpTopic } from '@/models/miscellaneous';
|
import { CstMatchMode, DependencyMode, GraphColoring, HelpTopic } from '@/models/miscellaneous';
|
||||||
import { ISubstitutionErrorDescription, OperationType, SubstitutionErrorType } from '@/models/oss';
|
import { ISubstitutionErrorDescription, OperationType, SubstitutionErrorType } from '@/models/oss';
|
||||||
import { CstClass, CstType, ExpressionStatus, IConstituenta, IRSForm } from '@/models/rsform';
|
import { CstClass, CstType, ExpressionStatus, IConstituenta, IRSForm } from '@/models/rsform';
|
||||||
import {
|
import {
|
||||||
|
@ -293,22 +292,6 @@ export function describeLocationHead(head: LocationHead): string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves label for graph layout mode.
|
|
||||||
*/
|
|
||||||
export const mapLabelLayout = new Map<GraphLayout, string>([
|
|
||||||
['treeTd2d', 'Граф: ДеревоВ 2D'],
|
|
||||||
['treeTd3d', 'Граф: ДеревоВ 3D'],
|
|
||||||
['forceatlas2', 'Граф: Атлас 2D'],
|
|
||||||
['forceDirected2d', 'Граф: Силы 2D'],
|
|
||||||
['forceDirected3d', 'Граф: Силы 3D'],
|
|
||||||
['treeLr2d', 'Граф: ДеревоГ 2D'],
|
|
||||||
['treeLr3d', 'Граф: ДеревоГ 3D'],
|
|
||||||
['radialOut2d', 'Граф: Радиус 2D'],
|
|
||||||
['radialOut3d', 'Граф: Радиус 3D'],
|
|
||||||
['circular2d', 'Граф: Круговая']
|
|
||||||
]);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves label for {@link GraphColoring}.
|
* Retrieves label for {@link GraphColoring}.
|
||||||
*/
|
*/
|
||||||
|
@ -319,15 +302,6 @@ export const mapLabelColoring = new Map<GraphColoring, string>([
|
||||||
['schemas', 'Цвет: Схемы']
|
['schemas', 'Цвет: Схемы']
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves label for {@link GraphSizing}.
|
|
||||||
*/
|
|
||||||
export const mapLabelSizing = new Map<GraphSizing, string>([
|
|
||||||
['none', 'Узлы: Моно'],
|
|
||||||
['derived', 'Узлы: Порожденные'],
|
|
||||||
['complex', 'Узлы: Простые']
|
|
||||||
]);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves label for {@link ExpressionStatus}.
|
* Retrieves label for {@link ExpressionStatus}.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -2,32 +2,20 @@
|
||||||
* Module: Mappings for selector UI elements. Do not confuse with html selectors
|
* Module: Mappings for selector UI elements. Do not confuse with html selectors
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { GraphLayout } from '@/components/ui/GraphUI';
|
|
||||||
import { type GramData, Grammeme, ReferenceType } from '@/models/language';
|
import { type GramData, Grammeme, ReferenceType } from '@/models/language';
|
||||||
import { grammemeCompare } from '@/models/languageAPI';
|
import { grammemeCompare } from '@/models/languageAPI';
|
||||||
import { GraphColoring, GraphSizing } from '@/models/miscellaneous';
|
import { GraphColoring } from '@/models/miscellaneous';
|
||||||
import { CstType } from '@/models/rsform';
|
import { CstType } from '@/models/rsform';
|
||||||
|
|
||||||
import { labelGrammeme, labelReferenceType, mapLabelColoring, mapLabelLayout, mapLabelSizing } from './labels';
|
import { labelGrammeme, labelReferenceType, mapLabelColoring } from './labels';
|
||||||
import { labelCstType } from './labels';
|
import { labelCstType } from './labels';
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents options for GraphLayout selector.
|
|
||||||
*/
|
|
||||||
export const SelectorGraphLayout: { value: GraphLayout; label: string }[] = //
|
|
||||||
[...mapLabelLayout.entries()].map(item => ({ value: item[0], label: item[1] }));
|
|
||||||
/**
|
/**
|
||||||
* Represents options for {@link GraphColoring} selector.
|
* Represents options for {@link GraphColoring} selector.
|
||||||
*/
|
*/
|
||||||
export const SelectorGraphColoring: { value: GraphColoring; label: string }[] = //
|
export const SelectorGraphColoring: { value: GraphColoring; label: string }[] = //
|
||||||
[...mapLabelColoring.entries()].map(item => ({ value: item[0], label: item[1] }));
|
[...mapLabelColoring.entries()].map(item => ({ value: item[0], label: item[1] }));
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents options for {@link GraphSizing} selector.
|
|
||||||
*/
|
|
||||||
export const SelectorGraphSizing: { value: GraphSizing; label: string }[] = //
|
|
||||||
[...mapLabelSizing.entries()].map(item => ({ value: item[0], label: item[1] }));
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents options for {@link CstType} selector.
|
* Represents options for {@link CstType} selector.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { defineConfig, loadEnv, PluginOption } from 'vite';
|
||||||
import { dependencies } from './package.json';
|
import { dependencies } from './package.json';
|
||||||
|
|
||||||
// Packages to include in main app bundle
|
// Packages to include in main app bundle
|
||||||
const inlinePackages = ['react', 'react-router-dom', 'react-dom'];
|
const inlinePackages = ['react', 'react-router', 'react-dom'];
|
||||||
|
|
||||||
// Rollup warnings that should not be displayed
|
// Rollup warnings that should not be displayed
|
||||||
const warningsToIgnore = [['SOURCEMAP_ERROR', "Can't resolve original location of error"]];
|
const warningsToIgnore = [['SOURCEMAP_ERROR', "Can't resolve original location of error"]];
|
||||||
|
@ -28,7 +28,7 @@ export default ({ mode }: { mode: string }) => {
|
||||||
sourcemap: false,
|
sourcemap: false,
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
output: {
|
||||||
manualChunks: { ...renderChunks(dependencies) }
|
manualChunks: { ...renderChunks(dependencies), manuals: ['./src/pages/ManualsPage'] }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue
Block a user