diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta.tsx index de5bbe0b..4f8a428d 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta.tsx @@ -241,7 +241,7 @@ function EditorConstituenta({ activeID, onShowAST, onCreateCst, onOpenEdit, onDe /> -
+
Клик на квадрат слева - выделение конституенты

Alt + вверх/вниз - движение конституент

Delete - удаление конституент

-

Alt + 1-6, Q,W - добавление конституент

+

Alt + 1-6,Q,W - добавление конституент

Статусы

{ [... mapStatusInfo.values()].map(info => { diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph.tsx index 13645b8d..0346dc0e 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph.tsx @@ -1,29 +1,70 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { darkTheme, GraphCanvas, GraphCanvasRef, GraphEdge, GraphNode, LayoutTypes, lightTheme, Sphere, useSelection } from 'reagraph'; +import { darkTheme, GraphCanvas, GraphCanvasRef, GraphEdge, + GraphNode, LayoutTypes, lightTheme, Sphere, useSelection +} from 'reagraph'; import Button from '../../components/Common/Button'; import Checkbox from '../../components/Common/Checkbox'; import ConceptSelect from '../../components/Common/ConceptSelect'; -import { ArrowsRotateIcon } from '../../components/Icons'; +import ConceptTooltip from '../../components/Common/ConceptTooltip'; +import Divider from '../../components/Common/Divider'; +import { ArrowsRotateIcon, HelpIcon } from '../../components/Icons'; import { useRSForm } from '../../context/RSFormContext'; import { useConceptTheme } from '../../context/ThemeContext'; import useLocalStorage from '../../hooks/useLocalStorage'; -import { resources } from '../../utils/constants'; +import { prefixes, resources } from '../../utils/constants'; import { Graph } from '../../utils/Graph'; -import { GraphLayoutSelector,mapLayoutLabels } from '../../utils/staticUI'; +import { IConstituenta } from '../../utils/models'; +import { getCstStatusColor, getCstTypeColor, getCstTypificationLabel, + GraphColoringSelector, GraphLayoutSelector, + mapColoringLabels, mapLayoutLabels, mapStatusInfo +} from '../../utils/staticUI'; +import ConstituentaTooltip from './elements/ConstituentaTooltip'; -function EditorTermGraph() { +export type ColoringScheme = 'none' | 'status' | 'type'; +const TREE_SIZE_MILESTONE = 50; + +function getCstNodeColor(cst: IConstituenta, coloringScheme: ColoringScheme, darkMode: boolean): string { + if (coloringScheme === 'type') { + return getCstTypeColor(cst.cstType, darkMode); + } + if (coloringScheme === 'status') { + return getCstStatusColor(cst.status, darkMode); + } + return ''; +} + +interface EditorTermGraphProps { + onOpenEdit: (cstID: number) => void + // onCreateCst: (selectedID: number | undefined, type: CstType | undefined, skipDialog?: boolean) => void + // onDeleteCst: (selected: number[], callback: (items: number[]) => void) => void +} + +function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) { const { schema } = useRSForm(); const { darkMode, noNavigation } = useConceptTheme(); + const [ layout, setLayout ] = useLocalStorage('graph_layout', 'treeTd2d'); - - const [ filtered, setFiltered ] = useState(new Graph()); + const [ coloringScheme, setColoringScheme ] = useLocalStorage('graph_coloring', 'none'); const [ orbit, setOrbit ] = useState(false); const [ noHermits, setNoHermits ] = useLocalStorage('graph_no_hermits', true); const [ noTransitive, setNoTransitive ] = useLocalStorage('graph_no_transitive', false); + + const [ filtered, setFiltered ] = useState(new Graph()); + const [ dismissed, setDismissed ] = useState([]); + const [ selectedDismissed, setSelectedDismissed ] = useState([]); const graphRef = useRef(null); + + const [hoverID, setHoverID] = useState(undefined); + const hoverCst = useMemo( + () => { + return schema?.items.find(cst => String(cst.id) == hoverID); + }, [schema?.items, hoverID]); - useEffect(() => { + const is3D = useMemo(() => layout.includes('3d'), [layout]); + + useEffect( + () => { if (!schema) { setFiltered(new Graph()); return; @@ -35,10 +76,32 @@ function EditorTermGraph() { if (noTransitive) { graph.transitiveReduction(); } + const newDismissed: number[] = []; + schema.items.forEach(cst => { + if (!graph.nodes.has(cst.id)) { + newDismissed.push(cst.id); + } + }); setFiltered(graph); + setDismissed(newDismissed); + setSelectedDismissed([]); + setHoverID(undefined); }, [schema, noHermits, noTransitive]); - const nodes: GraphNode[] = useMemo(() => { + function toggleDismissed(cstID: number) { + setSelectedDismissed(prev => { + const index = prev.findIndex(id => cstID == id); + if (index !== -1) { + prev.splice(index, 1); + } else { + prev.push(cstID); + } + return [... prev]; + }); + } + + const nodes: GraphNode[] = useMemo( + () => { const result: GraphNode[] = []; if (!schema) { return result; @@ -48,14 +111,16 @@ function EditorTermGraph() { if (cst) { result.push({ id: String(node.id), + fill: getCstNodeColor(cst, coloringScheme, darkMode), label: cst.term.resolved ? `${cst.alias}: ${cst.term.resolved}` : cst.alias }); } }); return result; - }, [schema, filtered.nodes]); + }, [schema, coloringScheme, filtered.nodes, darkMode]); - const edges: GraphEdge[] = useMemo(() => { + const edges: GraphEdge[] = useMemo( + () => { const result: GraphEdge[] = []; let edgeID = 1; filtered.nodes.forEach(source => { @@ -70,12 +135,7 @@ function EditorTermGraph() { }); return result; }, [filtered.nodes]); - - const handleCenter = useCallback(() => { - graphRef.current?.resetControls(); - graphRef.current?.centerGraph(); - }, []); - + const { selections, actives, onNodeClick, @@ -91,30 +151,92 @@ function EditorTermGraph() { focusOnSelect: false }); - const canvasSize = !noNavigation ? - 'w-[1240px] h-[736px] 2xl:w-[1880px] 2xl:h-[746px]' - : 'w-[1240px] h-[800px] 2xl:w-[1880px] 2xl:h-[810px]'; + const handleCenter = useCallback( + () => { + graphRef.current?.resetControls(); + graphRef.current?.centerGraph(); + }, []); - return (<> -
-
-
+ const handleHoverIn = useCallback( + (node: GraphNode) => { + setHoverID(node.id); + if (onNodePointerOver) onNodePointerOver(node); + }, [onNodePointerOver]); + + const handleHoverOut = useCallback( + (node: GraphNode) => { + setHoverID(undefined); + if (onNodePointerOut) onNodePointerOut(node); + }, [onNodePointerOut]); + + const handleNodeClick = useCallback( + (node: GraphNode) => { + if (selections.includes(node.id)) { + onOpenEdit(Number(node.id)); + return; + } + if (onNodeClick) onNodeClick(node); + }, [onNodeClick, selections, onOpenEdit]); + + const canvasWidth = useMemo( + () => { + return 'calc(100vw - 14.6rem)'; + }, []); + + const canvasHeight = useMemo( + () => { + return !noNavigation ? + 'calc(100vh - 13rem)' + : 'calc(100vh - 8.5rem)'; + }, [noNavigation]); + + const dismissedStyle = useCallback( + (cstID: number) => { + return selectedDismissed.includes(cstID) ? {outlineWidth: '2px', outlineStyle: 'solid'}: {}; + }, [selectedDismissed]); + + return ( +
+
+ {hoverCst && +
+
+

Конституента {hoverCst.alias}

+

Типизация: {getCstTypificationLabel(hoverCst)}

+

Термин: {hoverCst.term.resolved || hoverCst.term.raw}

+ {hoverCst.definition.formal &&

Выражение: {hoverCst.definition.formal}

} + {hoverCst.definition.text.resolved &&

Определение: {hoverCst.definition.text.resolved}

} + {hoverCst.convention &&

Конвенция: {hoverCst.convention}

} +
+
} +
+ { setLayout(data.length > 0 ? data[0].value : GraphLayoutSelector[0].value); }} + /> setOrbit(event.target.checked) } @@ -129,10 +251,75 @@ function EditorTermGraph() { value={noTransitive} onChange={ event => setNoTransitive(event.target.checked) } /> -
+ + + +
+

Скрытые конституенты

+
+ {dismissed.map(cstID => { + const cst = schema!.items.find(cst => cst.id === cstID)!; + const info = mapStatusInfo.get(cst.status)!; + return (<> +
toggleDismissed(cstID)} + onDoubleClick={() => onOpenEdit(cstID)} + > + {cst.alias} +
+ + ); + })} +
+
-
+
+
+ +
+ +
+

Настройка графа

+

Цвет - выбор правила покраски узлов

+

Скрытые конституенты окрашены в цвет статуса

+

Граф - выбор модели расположения узлов

+

Удалить несвязанные - в графе не отображаются одинокие вершины

+

Транзитивная редукция - в графе устраняются транзитивные пути

+ + + +

Горячие клавиши

+

Двойной клик - редактирование конституенты

+

Delete - удаление конституент

+

Alt + 1-6,Q,W - добавление конституент

+ + + +

Статусы

+ { [... mapStatusInfo.values()].map(info => { + return (

+ + {info.text} + + - + + {info.tooltip} + +

); + })} +
+
( @@ -156,7 +345,7 @@ function EditorTermGraph() { />
- ); +
); } diff --git a/rsconcept/frontend/src/pages/RSFormPage/RSTabs.tsx b/rsconcept/frontend/src/pages/RSFormPage/RSTabs.tsx index 87e673de..58ddc7c6 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/RSTabs.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/RSTabs.tsx @@ -40,7 +40,7 @@ function RSTabs() { const { destroySchema } = useLibrary(); const [activeTab, setActiveTab] = useState(RSTabsList.CARD); - const [activeID, setActiveID] = useState(undefined) + const [activeID, setActiveID] = useState(undefined); const [showUpload, setShowUpload] = useState(false); const [showClone, setShowClone] = useState(false); @@ -264,7 +264,9 @@ function RSTabs() { - + } diff --git a/rsconcept/frontend/src/utils/staticUI.ts b/rsconcept/frontend/src/utils/staticUI.ts index ba884892..1045e83e 100644 --- a/rsconcept/frontend/src/utils/staticUI.ts +++ b/rsconcept/frontend/src/utils/staticUI.ts @@ -1,5 +1,6 @@ import { LayoutTypes } from 'reagraph'; +import { ColoringScheme } from '../pages/RSFormPage/EditorTermGraph'; import { resolveErrorClass,RSErrorClass, RSErrorType, TokenID } from './enums'; import { CstMatchMode, CstType, DependencyMode,ExpressionStatus, IConstituenta, IFunctionArg,IRSErrorDescription, IRSForm, @@ -244,28 +245,16 @@ export function getCstTypeShortcut(type: CstType) { } } +export const mapCstTypeColors: Map = new Map([ + [CstType.BASE, 'Атлас 2D'], +]); + export const CstTypeSelector = (Object.values(CstType)).map( (typeStr) => { const type = typeStr as CstType; return { value: type, label: getCstTypeLabel(type) }; }); -export const mapLayoutLabels: Map = new Map([ - ['forceatlas2', 'Атлас 2D'], - ['forceDirected2d', 'Силы 2D'], - ['forceDirected3d', 'Силы 3D'], - ['treeTd2d', 'ДеревоВерт 2D'], - ['treeTd3d', 'ДеревоВерт 3D'], - ['treeLr2d', 'ДеревоГор 2D'], - ['treeLr3d', 'ДеревоГор 3D'], - ['radialOut2d', 'Радиальная 2D'], - ['radialOut3d', 'Радиальная 3D'], - ['circular2d', 'Круговая'], - ['hierarchicalTd', 'ИерархияВерт'], - ['hierarchicalLr', 'ИерархияГор'], - ['nooverlap', 'Без перекрытия'] -]); - export function getCstCompareLabel(mode: CstMatchMode): string { switch(mode) { case CstMatchMode.ALL: return 'везде'; @@ -288,21 +277,73 @@ export function getDependencyLabel(mode: DependencyMode): string { } export const GraphLayoutSelector: {value: LayoutTypes, label: string}[] = [ - { value: 'forceatlas2', label: 'Атлас 2D'}, - { value: 'forceDirected2d', label: 'Силы 2D'}, - { value: 'forceDirected3d', label: 'Силы 3D'}, - { value: 'treeTd2d', label: 'ДеревоВ 2D'}, - { value: 'treeTd3d', label: 'ДеревоВ 3D'}, - { value: 'treeLr2d', label: 'ДеревоГ 2D'}, - { value: 'treeLr3d', label: 'ДеревоГ 3D'}, - { value: 'radialOut2d', label: 'Радиальная 2D'}, - { value: 'radialOut3d', label: 'Радиальная 3D'}, + { value: 'treeTd2d', label: 'Граф: ДеревоВ 2D'}, + { value: 'treeTd3d', label: 'Граф: ДеревоВ 3D'}, + { value: 'forceatlas2', label: 'Граф: Атлас 2D'}, + { value: 'forceDirected2d', label: 'Граф: Силы 2D'}, + { value: 'forceDirected3d', label: 'Граф: Силы 3D'}, + { value: 'treeLr2d', label: 'Граф: ДеревоГ 2D'}, + { value: 'treeLr3d', label: 'Граф: ДеревоГ 3D'}, + { value: 'radialOut2d', label: 'Граф: Радиальная 2D'}, + { value: 'radialOut3d', label: 'Граф: Радиальная 3D'}, // { value: 'circular2d', label: 'circular2d'}, // { value: 'nooverlap', label: 'nooverlap'}, // { value: 'hierarchicalTd', label: 'hierarchicalTd'}, // { value: 'hierarchicalLr', label: 'hierarchicalLr'} ]; +export const mapLayoutLabels: Map = new Map([ + ['forceatlas2', 'Граф: Атлас 2D'], + ['forceDirected2d', 'Граф: Силы 2D'], + ['forceDirected3d', 'Граф: Силы 3D'], + ['treeTd2d', 'Граф: ДеревоВерт 2D'], + ['treeTd3d', 'Граф: ДеревоВерт 3D'], + ['treeLr2d', 'Граф: ДеревоГор 2D'], + ['treeLr3d', 'Граф: ДеревоГор 3D'], + ['radialOut2d', 'Граф: Радиальная 2D'], + ['radialOut3d', 'Граф: Радиальная 3D'], + ['circular2d', 'Граф: Круговая'], + ['hierarchicalTd', 'Граф: ИерархияВерт'], + ['hierarchicalLr', 'Граф: ИерархияГор'], + ['nooverlap', 'Граф: Без перекрытия'] +]); + +export const mapColoringLabels: Map = new Map([ + ['none', 'Цвет: моно'], + ['status', 'Цвет: статус'], + ['type', 'Цвет: тип'], +]); + +export const GraphColoringSelector: {value: ColoringScheme, label: string}[] = [ + { value: 'none', label: 'Цвет: моно'}, + { value: 'status', label: 'Цвет: статус'}, + { value: 'type', label: 'Цвет: тип'}, +]; + +export function getCstTypeColor(type: CstType, darkMode: boolean): string { + switch (type) { + case CstType.BASE: return darkMode ? '#2b8000': '#aaff80'; + case CstType.CONSTANT: return darkMode ? '#2b8000': '#aaff80'; + case CstType.STRUCTURED: return darkMode ? '#2b8000': '#aaff80'; + case CstType.TERM: return darkMode ? '#1e00b3': '#b3bdff'; + case CstType.FUNCTION: return darkMode ? '#1e00b3': '#b3bdff'; + case CstType.AXIOM: return darkMode ? '#592b2b': '#ffc9c9'; + case CstType.PREDICATE: return darkMode ? '#1e00b3': '#b3bdff'; + case CstType.THEOREM: return darkMode ? '#592b2b': '#ffc9c9'; + } +} + +export function getCstStatusColor(status: ExpressionStatus, darkMode: boolean): string { + switch (status) { + case ExpressionStatus.VERIFIED: return darkMode ? '#2b8000': '#aaff80'; + case ExpressionStatus.INCORRECT: return darkMode ? '#592b2b': '#ffc9c9'; + case ExpressionStatus.INCALCULABLE: return darkMode ? '#964600': '#ffbb80'; + case ExpressionStatus.PROPERTY: return darkMode ? '#36899e': '#a5e9fa'; + case ExpressionStatus.UNKNOWN: return darkMode ? '#1e00b3': '#b3bdff'; + case ExpressionStatus.UNDEFINED: return darkMode ? '#1e00b3': '#b3bdff'; + } +} + export const mapStatusInfo: Map = new Map([ [ ExpressionStatus.VERIFIED, { text: 'ок',