diff --git a/rsconcept/frontend/src/features/rsform/pages/RSFormPage/EditorTermGraph/GraphSelectors.tsx b/rsconcept/frontend/src/features/rsform/pages/RSFormPage/EditorTermGraph/GraphSelectors.tsx index 64081280..588579c7 100644 --- a/rsconcept/frontend/src/features/rsform/pages/RSFormPage/EditorTermGraph/GraphSelectors.tsx +++ b/rsconcept/frontend/src/features/rsform/pages/RSFormPage/EditorTermGraph/GraphSelectors.tsx @@ -4,8 +4,7 @@ import { Overlay } from '@/components/Container'; import { SelectSingle } from '@/components/Input'; import { mapLabelColoring } from '../../../labels'; -import { type IRSForm } from '../../../models/rsform'; -import { type GraphColoring } from '../../../stores/termGraph'; +import { type GraphColoring, useTermGraphStore } from '../../../stores/termGraph'; import { SchemasGuide } from './SchemasGuide'; @@ -15,19 +14,16 @@ import { SchemasGuide } from './SchemasGuide'; const SelectorGraphColoring: { value: GraphColoring; label: string }[] = // [...mapLabelColoring.entries()].map(item => ({ value: item[0], label: item[1] })); -interface GraphSelectorsProps { - schema: IRSForm; - coloring: GraphColoring; - onChangeColoring: (newValue: GraphColoring) => void; -} +export function GraphSelectors() { + const coloring = useTermGraphStore(state => state.coloring); + const setColoring = useTermGraphStore(state => state.setColoring); -export function GraphSelectors({ schema, coloring, onChangeColoring }: GraphSelectorsProps) { return (
{coloring === 'status' ? : null} {coloring === 'type' ? : null} - {coloring === 'schemas' ? : null} + {coloring === 'schemas' ? : null} onChangeColoring(data?.value ?? SelectorGraphColoring[0].value)} + onChange={data => setColoring(data?.value ?? SelectorGraphColoring[0].value)} />
); diff --git a/rsconcept/frontend/src/features/rsform/pages/RSFormPage/EditorTermGraph/SchemasGuide.tsx b/rsconcept/frontend/src/features/rsform/pages/RSFormPage/EditorTermGraph/SchemasGuide.tsx index ddfa12bd..40dd95c0 100644 --- a/rsconcept/frontend/src/features/rsform/pages/RSFormPage/EditorTermGraph/SchemasGuide.tsx +++ b/rsconcept/frontend/src/features/rsform/pages/RSFormPage/EditorTermGraph/SchemasGuide.tsx @@ -5,14 +5,11 @@ import { IconHelp } from '@/components/Icons'; import { globalIDs, prefixes } from '@/utils/constants'; import { colorBgSchemas } from '../../../colors'; -import { type IRSForm } from '../../../models/rsform'; +import { useRSEdit } from '../RSEditContext'; -interface SchemasGuideProps { - schema: IRSForm; -} - -export function SchemasGuide({ schema }: SchemasGuideProps) { +export function SchemasGuide() { const { items: libraryItems } = useLibrary(); + const { schema } = useRSEdit(); const schemas = (() => { const processed = new Set(); diff --git a/rsconcept/frontend/src/features/rsform/pages/RSFormPage/EditorTermGraph/SelectedCounter.tsx b/rsconcept/frontend/src/features/rsform/pages/RSFormPage/EditorTermGraph/SelectedCounter.tsx deleted file mode 100644 index 4aaaf5d5..00000000 --- a/rsconcept/frontend/src/features/rsform/pages/RSFormPage/EditorTermGraph/SelectedCounter.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Overlay } from '@/components/Container'; - -interface SelectedCounterProps { - totalCount: number; - selectedCount: number; - position?: string; -} - -export function SelectedCounter({ totalCount, selectedCount, position = 'top-0 left-0' }: SelectedCounterProps) { - if (selectedCount === 0) { - return null; - } - return ( - - Выбор {selectedCount} из {totalCount} - - ); -} diff --git a/rsconcept/frontend/src/features/rsform/pages/RSFormPage/EditorTermGraph/TGFlow.tsx b/rsconcept/frontend/src/features/rsform/pages/RSFormPage/EditorTermGraph/TGFlow.tsx index af091168..47c8c13b 100644 --- a/rsconcept/frontend/src/features/rsform/pages/RSFormPage/EditorTermGraph/TGFlow.tsx +++ b/rsconcept/frontend/src/features/rsform/pages/RSFormPage/EditorTermGraph/TGFlow.tsx @@ -15,30 +15,27 @@ import { import { Overlay } from '@/components/Container'; import { useMainHeight } from '@/stores/appLayout'; -import { APP_COLORS } from '@/styling/colors'; import { PARAMETER } from '@/utils/constants'; -import { CstType } from '../../../backend/types'; import { useMutatingRSForm } from '../../../backend/useMutatingRSForm'; -import { colorBgGraphNode } from '../../../colors'; import { ToolbarGraphSelection } from '../../../components/ToolbarGraphSelection'; -import { type IConstituenta, type IRSForm } from '../../../models/rsform'; +import { type IConstituenta } from '../../../models/rsform'; import { isBasicConcept } from '../../../models/rsformAPI'; -import { type GraphFilterParams, useTermGraphStore } from '../../../stores/termGraph'; +import { useTermGraphStore } from '../../../stores/termGraph'; import { useRSEdit } from '../RSEditContext'; import { TGEdgeTypes } from './graph/TGEdgeTypes'; import { applyLayout } from './graph/TGLayout'; -import { type TGNodeData } from './graph/TGNode'; import { TGNodeTypes } from './graph/TGNodeTypes'; import { GraphSelectors } from './GraphSelectors'; -import { SelectedCounter } from './SelectedCounter'; import { ToolbarFocusedCst } from './ToolbarFocusedCst'; import { ToolbarTermGraph } from './ToolbarTermGraph'; +import { useFilteredGraph } from './useFilteredGraph'; import { ViewHidden } from './ViewHidden'; export const ZOOM_MAX = 3; export const ZOOM_MIN = 0.25; +export const VIEW_PADDING = 0.3; export function TGFlow() { const mainHeight = useMainHeight(); @@ -51,50 +48,38 @@ export function TGFlow() { schema, selected, setSelected, - navigateCst, - toggleSelect, canDeleteSelected, - promptDeleteCst + promptDeleteCst, + focusCst, + setFocus, + deselectAll } = useRSEdit(); - const filter = useTermGraphStore(state => state.filter); - const coloring = useTermGraphStore(state => state.coloring); - const setColoring = useTermGraphStore(state => state.setColoring); - + const [needReset, setNeedReset] = useState(true); const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges] = useEdgesState([]); - const [focusCst, setFocusCst] = useState(null); - const filteredGraph = produceFilteredGraph(schema, filter, focusCst); - const [hidden, setHidden] = useState([]); - - const [needReset, setNeedReset] = useState(true); + const filter = useTermGraphStore(state => state.filter); + const { filteredGraph, hidden } = useFilteredGraph(); function onSelectionChange({ nodes }: { nodes: Node[] }) { const ids = nodes.map(node => Number(node.id)); if (ids.length === 0) { - setSelected([]); + deselectAll(); } else { setSelected(prev => [...prev.filter(nodeID => !filteredGraph.hasNode(nodeID)), ...ids]); } } - useOnSelectionChange({ onChange: onSelectionChange }); useEffect(() => { - const newDismissed: number[] = []; - schema.items.forEach(cst => { - if (!filteredGraph.nodes.has(cst.id)) { - newDismissed.push(cst.id); - } - }); - setHidden(newDismissed); - }, [schema, filteredGraph]); + setNeedReset(true); + }, [schema, filter, focusCst]); const resetNodes = useCallback(() => { - const newNodes: Node[] = []; + const newNodes: Node[] = []; filteredGraph.nodes.forEach(node => { const cst = schema.cstByID.get(node.id); if (cst) { @@ -103,10 +88,7 @@ export function TGFlow() { type: 'concept', selected: selected.includes(node.id), position: { x: 0, y: 0 }, - data: { - fill: focusCst === cst ? APP_COLORS.bgPurple : colorBgGraphNode(cst, coloring), - cst: cst - } + data: cst }); } }); @@ -137,11 +119,11 @@ export function TGFlow() { setNodes(newNodes); setEdges(newEdges); - }, [schema, filteredGraph, setNodes, setEdges, filter.noText, selected, focusCst, coloring]); - useEffect(() => { - setNeedReset(true); - }, [schema, focusCst, coloring, filter]); + setTimeout(() => { + fitView({ duration: PARAMETER.zoomDuration, padding: VIEW_PADDING }); + }, PARAMETER.minimalTimeout); + }, [schema, filteredGraph, setNodes, setEdges, filter.noText, selected, fitView]); useEffect(() => { if (!needReset || !viewportInitialized) { @@ -163,8 +145,7 @@ export function TGFlow() { if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); - handleSetFocus(null); - handleSetSelected([]); + setFocus(null); return; } if (!isContentEditable) { @@ -180,36 +161,11 @@ export function TGFlow() { } } - function handleSetFocus(cstID: number | null) { - if (cstID === null) { - setFocusCst(null); - } else { - const target = schema.cstByID.get(cstID) ?? null; - setFocusCst(prev => (prev === target ? null : target)); - } - setSelected([]); - setTimeout(() => { - fitView({ duration: PARAMETER.zoomDuration }); - }, PARAMETER.minimalTimeout); - } - - function handleNodeContextMenu(event: React.MouseEvent, cstID: number) { - event.preventDefault(); - event.stopPropagation(); - handleSetFocus(cstID); - } - - function handleNodeDoubleClick(event: React.MouseEvent, cstID: number) { - event.preventDefault(); - event.stopPropagation(); - navigateCst(cstID); - } - return ( <> - {focusCst ? handleSetFocus(null)} /> : null} + {!focusCst ? (
- - - -
- - + +
+ Выбор {selected.length} из {schema.stats?.count_all ?? 0}
+ +
@@ -258,71 +205,10 @@ export function TGFlow() { edgeTypes={TGEdgeTypes} maxZoom={ZOOM_MAX} minZoom={ZOOM_MIN} - onNodeDoubleClick={(event, node) => handleNodeDoubleClick(event, Number(node.id))} - onNodeContextMenu={(event, node) => handleNodeContextMenu(event, Number(node.id))} + onContextMenu={event => event.preventDefault()} />
); } - -// ====== Internals ========= -function produceFilteredGraph(schema: IRSForm, params: GraphFilterParams, focusCst: IConstituenta | null) { - const filtered = schema.graph.clone(); - const allowedTypes: CstType[] = (() => { - const result: CstType[] = []; - if (params.allowBase) result.push(CstType.BASE); - if (params.allowStruct) result.push(CstType.STRUCTURED); - if (params.allowTerm) result.push(CstType.TERM); - if (params.allowAxiom) result.push(CstType.AXIOM); - if (params.allowFunction) result.push(CstType.FUNCTION); - if (params.allowPredicate) result.push(CstType.PREDICATE); - if (params.allowConstant) result.push(CstType.CONSTANT); - if (params.allowTheorem) result.push(CstType.THEOREM); - return result; - })(); - - if (params.noHermits) { - filtered.removeIsolated(); - } - if (params.noTemplates) { - schema.items.forEach(cst => { - if (cst !== focusCst && cst.is_template) { - filtered.foldNode(cst.id); - } - }); - } - if (allowedTypes.length < Object.values(CstType).length) { - schema.items.forEach(cst => { - if (cst !== focusCst && !allowedTypes.includes(cst.cst_type)) { - filtered.foldNode(cst.id); - } - }); - } - if (!focusCst && params.foldDerived) { - schema.items.forEach(cst => { - if (cst.spawner) { - filtered.foldNode(cst.id); - } - }); - } - if (focusCst) { - const includes: number[] = [ - focusCst.id, - ...focusCst.spawn, - ...(params.focusShowInputs ? schema.graph.expandInputs([focusCst.id]) : []), - ...(params.focusShowOutputs ? schema.graph.expandOutputs([focusCst.id]) : []) - ]; - schema.items.forEach(cst => { - if (!includes.includes(cst.id)) { - filtered.foldNode(cst.id); - } - }); - } - if (params.noTransitive) { - filtered.transitiveReduction(); - } - - return filtered; -} diff --git a/rsconcept/frontend/src/features/rsform/pages/RSFormPage/EditorTermGraph/ToolbarFocusedCst.tsx b/rsconcept/frontend/src/features/rsform/pages/RSFormPage/EditorTermGraph/ToolbarFocusedCst.tsx index 0cac5ab5..6e0c4da7 100644 --- a/rsconcept/frontend/src/features/rsform/pages/RSFormPage/EditorTermGraph/ToolbarFocusedCst.tsx +++ b/rsconcept/frontend/src/features/rsform/pages/RSFormPage/EditorTermGraph/ToolbarFocusedCst.tsx @@ -6,23 +6,16 @@ import { MiniButton } from '@/components/Control'; import { IconGraphInputs, IconGraphOutputs, IconReset } from '@/components/Icons'; import { APP_COLORS } from '@/styling/colors'; -import { type IConstituenta } from '../../../models/rsform'; import { useRSEdit } from '../RSEditContext'; -interface ToolbarFocusedCstProps { - focusedCst: IConstituenta; - onResetFocus: () => void; -} - -export function ToolbarFocusedCst({ focusedCst, onResetFocus }: ToolbarFocusedCstProps) { - const { deselectAll } = useRSEdit(); +export function ToolbarFocusedCst() { + const { setFocus, focusCst } = useRSEdit(); const filter = useTermGraphStore(state => state.filter); const setFilter = useTermGraphStore(state => state.setFilter); function resetSelection() { - onResetFocus(); - deselectAll(); + setFocus(null); } function handleShowInputs() { @@ -39,11 +32,15 @@ export function ToolbarFocusedCst({ focusedCst, onResetFocus }: ToolbarFocusedCs }); } + if (!focusCst) { + return null; + } + return (
Фокус - {focusedCst.alias} + {focusCst.alias}
state.showGraphParams); const filter = useTermGraphStore(state => state.filter); const setFilter = useTermGraphStore(state => state.setFilter); - const nodes = useNodes(); - const flow = useReactFlow(); + + const { fitView, getNodes } = useReactFlow(); function handleShowTypeGraph() { const typeInfo = schema.items.map(item => ({ @@ -88,7 +88,7 @@ export function ToolbarTermGraph() { const imageWidth = PARAMETER.ossImageWidth; const imageHeight = PARAMETER.ossImageHeight; - const nodesBounds = getNodesBounds(nodes); + const nodesBounds = getNodesBounds(getNodes()); const viewport = getViewportForBounds(nodesBounds, imageWidth, imageHeight, ZOOM_MIN, ZOOM_MAX); toPng(canvas, { backgroundColor: darkMode ? APP_COLORS.bgDefaultDark : APP_COLORS.bgDefaultLight, @@ -114,7 +114,7 @@ export function ToolbarTermGraph() { function handleFitView() { setTimeout(() => { - flow.fitView({ duration: PARAMETER.zoomDuration }); + fitView({ duration: PARAMETER.zoomDuration, padding: VIEW_PADDING }); }, PARAMETER.minimalTimeout); } @@ -123,9 +123,6 @@ export function ToolbarTermGraph() { ...filter, foldDerived: !filter.foldDerived }); - setTimeout(() => { - flow.fitView({ duration: PARAMETER.zoomDuration }); - }, PARAMETER.graphRefreshDelay); } return ( diff --git a/rsconcept/frontend/src/features/rsform/pages/RSFormPage/EditorTermGraph/ViewHidden.tsx b/rsconcept/frontend/src/features/rsform/pages/RSFormPage/EditorTermGraph/ViewHidden.tsx index d095c681..6821b0c5 100644 --- a/rsconcept/frontend/src/features/rsform/pages/RSFormPage/EditorTermGraph/ViewHidden.tsx +++ b/rsconcept/frontend/src/features/rsform/pages/RSFormPage/EditorTermGraph/ViewHidden.tsx @@ -12,36 +12,35 @@ import { APP_COLORS } from '@/styling/colors'; import { globalIDs, PARAMETER, prefixes } from '@/utils/constants'; import { colorBgGraphNode } from '../../../colors'; -import { type IRSForm } from '../../../models/rsform'; -import { type GraphColoring, useTermGraphStore } from '../../../stores/termGraph'; +import { type IConstituenta } from '../../../models/rsform'; +import { useTermGraphStore } from '../../../stores/termGraph'; import { useRSEdit } from '../RSEditContext'; interface ViewHiddenProps { - schema: IRSForm; items: number[]; - selected: number[]; - coloringScheme: GraphColoring; - - toggleSelection: (cstID: number) => void; - setFocus: (cstID: number) => void; } -export function ViewHidden({ items, selected, toggleSelection, setFocus, schema, coloringScheme }: ViewHiddenProps) { +export function ViewHidden({ items }: ViewHiddenProps) { const windowSize = useWindowSize(); - const localSelected = items.filter(id => selected.includes(id)); + const coloring = useTermGraphStore(state => state.coloring); + const { navigateCst, setFocus, schema, selected, toggleSelect } = useRSEdit(); - const { navigateCst } = useRSEdit(); + const localSelected = items.filter(id => selected.includes(id)); const isFolded = useTermGraphStore(state => state.foldHidden); const toggleFolded = useTermGraphStore(state => state.toggleFoldHidden); const setActiveCst = useTooltipsStore(state => state.setActiveCst); const hiddenHeight = useFitHeight(windowSize.isSmall ? '10.4rem + 2px' : '12.5rem + 2px'); - function handleClick(cstID: number, event: React.MouseEvent) { - if (event.ctrlKey || event.metaKey) { - setFocus(cstID); - } else { - toggleSelection(cstID); - } + function handleClick(event: React.MouseEvent, cstID: number) { + event.preventDefault(); + event.stopPropagation(); + toggleSelect(cstID); + } + + function handleContextMenu(event: React.MouseEvent, target: IConstituenta) { + event.stopPropagation(); + event.preventDefault(); + setFocus(target); } if (items.length <= 0) { @@ -92,14 +91,13 @@ export function ViewHidden({ items, selected, toggleSelection, setFocus, schema, > {items.map(cstID => { const cst = schema.cstByID.get(cstID)!; - const id = `${prefixes.cst_hidden_list}${cst.alias}`; return (