import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; 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 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 { prefixes, resources } from '../../utils/constants'; import { Graph } from '../../utils/Graph'; import { IConstituenta } from '../../utils/models'; import { getCstStatusColor, getCstTypeColor, getCstTypificationLabel, GraphColoringSelector, GraphLayoutSelector, mapColoringLabels, mapLayoutLabels, mapStatusInfo } from '../../utils/staticUI'; import ConstituentaTooltip from './elements/ConstituentaTooltip'; 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 [ 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]); const is3D = useMemo(() => layout.includes('3d'), [layout]); useEffect( () => { if (!schema) { setFiltered(new Graph()); return; } const graph = schema.graph.clone(); if (noHermits) { graph.removeIsolated(); } 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]); 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; } filtered.nodes.forEach(node => { const cst = schema.items.find(cst => cst.id === node.id); 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, coloringScheme, filtered.nodes, darkMode]); const edges: GraphEdge[] = useMemo( () => { const result: GraphEdge[] = []; let edgeID = 1; filtered.nodes.forEach(source => { source.outputs.forEach(target => { result.push({ id: String(edgeID), source: String(source.id), target: String(target) }); edgeID += 1; }); }); return result; }, [filtered.nodes]); const { selections, actives, onNodeClick, onCanvasClick, onNodePointerOver, onNodePointerOut } = useSelection({ ref: graphRef, nodes, edges, type: 'multi', // 'single' | 'multi' | 'multiModifier' pathSelectionType: 'all', focusOnSelect: false }); const handleCenter = useCallback( () => { graphRef.current?.resetControls(); graphRef.current?.centerGraph(); }, []); 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) } /> setNoHermits(event.target.checked) } /> 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}

); })}
( )} />
); } export default EditorTermGraph;