diff --git a/rsconcept/frontend/src/components/man/HelpConstituenta.tsx b/rsconcept/frontend/src/components/man/HelpConstituenta.tsx index f3238510..89437114 100644 --- a/rsconcept/frontend/src/components/man/HelpConstituenta.tsx +++ b/rsconcept/frontend/src/components/man/HelpConstituenta.tsx @@ -6,7 +6,7 @@ function HelpConstituenta() { return (

Редактор конституент

-

При выделении также подсвечиваются производные и основание

+

Помимо активной конституенты выделяются порожденные и основание

Сохранить изменения: Ctrl + S или клик по кнопке Сохранить

Формальное определение

- Ctrl + Пробел дополняет до незанятого имени

diff --git a/rsconcept/frontend/src/dialogs/DlgGraphParams.tsx b/rsconcept/frontend/src/dialogs/DlgGraphParams.tsx index ee7e0010..280dc0ce 100644 --- a/rsconcept/frontend/src/dialogs/DlgGraphParams.tsx +++ b/rsconcept/frontend/src/dialogs/DlgGraphParams.tsx @@ -56,8 +56,8 @@ function DlgGraphParams({ hideWindow, initial, onConfirm }: DlgGraphParamsProps) setValue={value => updateParams({ noTransitive: value })} /> updateParams({ foldDerived: value })} /> diff --git a/rsconcept/frontend/src/models/Graph.ts b/rsconcept/frontend/src/models/Graph.ts index f65930c2..684f924e 100644 --- a/rsconcept/frontend/src/models/Graph.ts +++ b/rsconcept/frontend/src/models/Graph.ts @@ -116,6 +116,7 @@ export class Graph { const result: GraphNode[] = []; this.nodes.forEach(node => { if (node.outputs.length === 0 && node.inputs.length === 0) { + result.push(node); this.nodes.delete(node.id); } }); diff --git a/rsconcept/frontend/src/models/miscellaneous.ts b/rsconcept/frontend/src/models/miscellaneous.ts index a1430808..a946986b 100644 --- a/rsconcept/frontend/src/models/miscellaneous.ts +++ b/rsconcept/frontend/src/models/miscellaneous.ts @@ -103,6 +103,9 @@ export interface GraphFilterParams { noText: boolean; foldDerived: boolean; + focusShowInputs: boolean; + focusShowOutputs: boolean; + allowBase: boolean; allowStruct: boolean; allowTerm: boolean; diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/EditorTermGraph.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/EditorTermGraph.tsx index 4f755081..ca530738 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/EditorTermGraph.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/EditorTermGraph.tsx @@ -7,6 +7,7 @@ import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'; import InfoConstituenta from '@/components/info/InfoConstituenta'; import SelectedCounter from '@/components/info/SelectedCounter'; +import SelectGraphToolbar from '@/components/select/SelectGraphToolbar'; import { GraphCanvasRef, GraphEdge, GraphLayout, GraphNode } from '@/components/ui/GraphUI'; import Overlay from '@/components/ui/Overlay'; import { useConceptOptions } from '@/context/OptionsContext'; @@ -14,12 +15,14 @@ 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 } from '@/models/rsform'; +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 FocusToolbar from './FocusToolbar'; import GraphSelectors from './GraphSelectors'; import GraphToolbar from './GraphToolbar'; import TermGraph from './TermGraph'; @@ -41,6 +44,9 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) { noText: false, foldDerived: false, + focusShowInputs: false, + focusShowOutputs: false, + allowBase: true, allowStruct: true, allowTerm: true, @@ -51,7 +57,8 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) { allowTheorem: true }); const [showParamsDialog, setShowParamsDialog] = useState(false); - const filtered = useGraphFilter(controller.schema, filterParams); + const [focusCst, setFocusCst] = useState(undefined); + const filtered = useGraphFilter(controller.schema, filterParams, focusCst); const graphRef = useRef(null); const [hidden, setHidden] = useState([]); @@ -93,7 +100,7 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) { if (cst) { result.push({ id: String(node.id), - fill: colorBgGraphNode(cst, coloring, colors), + fill: focusCst === cst ? colors.bgPurple : colorBgGraphNode(cst, coloring, colors), label: cst.alias, subLabel: !filterParams.noText ? cst.term_resolved : undefined, size: applyNodeSizing(cst, sizing) @@ -101,23 +108,25 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) { } }); return result; - }, [controller.schema, coloring, sizing, filtered.nodes, filterParams.noText, colors]); + }, [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 => { - result.push({ - id: String(edgeID), - source: String(source.id), - target: String(target) - }); - edgeID += 1; + 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]); + }, [filtered.nodes, nodes]); function handleCreateCst() { if (!controller.schema) { @@ -174,6 +183,7 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) { } if (event.key === 'Escape') { event.preventDefault(); + setFocusCst(undefined); controller.deselectAll(); } } @@ -188,6 +198,17 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) { }, 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( () => ( ), @@ -217,7 +239,8 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) { onOpenEdit, toggleResetView, controller.select, - controller.deselect + controller.deselect, + handleSetFocus ] ); @@ -240,25 +263,57 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) { position='top-[4.3rem] sm:top-[0.3rem] left-0' /> - setShowParamsDialog(true)} - onCreate={handleCreateCst} - onDelete={handleDeleteCst} - onResetViewpoint={() => setToggleResetView(prev => !prev)} - onSaveImage={handleSaveImage} - toggleOrbit={() => setOrbit(prev => !prev)} - toggleFoldDerived={handleFoldDerived} - toggleNoText={() => - setFilterParams(prev => ({ - ...prev, - noText: !prev.noText - })) - } - /> + + setShowParamsDialog(true)} + onCreate={handleCreateCst} + onDelete={handleDeleteCst} + onResetViewpoint={() => setToggleResetView(prev => !prev)} + onSaveImage={handleSaveImage} + toggleOrbit={() => setOrbit(prev => !prev)} + toggleFoldDerived={handleFoldDerived} + toggleNoText={() => + setFilterParams(prev => ({ + ...prev, + noText: !prev.noText + })) + } + /> + {!focusCst ? ( + isBasicConcept(cst.cst_type)).map(cst => cst.id)} + setSelected={controller.setSelected} + /> + ) : null} + {focusCst ? ( + handleSetFocus(undefined)} + showInputs={filterParams.focusShowInputs} + showOutputs={filterParams.focusShowOutputs} + toggleShowInputs={() => + setFilterParams(prev => ({ + ...prev, + focusShowInputs: !prev.focusShowInputs + })) + } + toggleShowOutputs={() => + setFilterParams(prev => ({ + ...prev, + focusShowOutputs: !prev.focusShowOutputs + })) + } + /> + ) : null} + {hoverCst ? (
diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/FocusToolbar.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/FocusToolbar.tsx new file mode 100644 index 00000000..b56243fb --- /dev/null +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/FocusToolbar.tsx @@ -0,0 +1,75 @@ +'use client'; + +import { useCallback } from 'react'; + +import { IconGraphInputs, IconGraphOutputs, IconReset } from '@/components/Icons'; +import MiniButton from '@/components/ui/MiniButton'; +import { useConceptOptions } from '@/context/OptionsContext'; +import { IConstituenta } from '@/models/rsform'; + +import { useRSEdit } from '../RSEditContext'; + +interface FocusToolbarProps { + center: IConstituenta; + showInputs: boolean; + showOutputs: boolean; + + reset: () => void; + toggleShowInputs: () => void; + toggleShowOutputs: () => void; +} + +function FocusToolbar({ + center, + reset, + showInputs, + showOutputs, + toggleShowInputs, + toggleShowOutputs +}: FocusToolbarProps) { + const { colors } = useConceptOptions(); + const controller = useRSEdit(); + + const resetSelection = useCallback(() => { + reset(); + controller.setSelected([]); + }, [reset, controller]); + + return ( +
+
+ Фокус + {center.alias} +
+ } + onClick={resetSelection} + /> + + ) : ( + + ) + } + onClick={toggleShowInputs} + /> + + ) : ( + + ) + } + onClick={toggleShowOutputs} + /> +
+ ); +} + +export default FocusToolbar; diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/GraphToolbar.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/GraphToolbar.tsx index db042a0b..1a1a4eb5 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/GraphToolbar.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/GraphToolbar.tsx @@ -1,5 +1,3 @@ -'use client'; - import { IconClustering, IconClusteringOff, @@ -13,11 +11,8 @@ import { IconTextOff } from '@/components/Icons'; import BadgeHelp from '@/components/man/BadgeHelp'; -import SelectGraphToolbar from '@/components/select/SelectGraphToolbar'; import MiniButton from '@/components/ui/MiniButton'; -import Overlay from '@/components/ui/Overlay'; import { HelpTopic } from '@/models/miscellaneous'; -import { isBasicConcept } from '@/models/rsformAPI'; import { useRSEdit } from '../RSEditContext'; @@ -56,78 +51,68 @@ function GraphToolbar({ const controller = useRSEdit(); return ( - -
- } - onClick={showParamsDialog} - /> - } - title='Граф целиком' - onClick={onResetViewpoint} - /> - - ) : ( - - ) - } - onClick={toggleNoText} - /> - - ) : ( - - ) - } - onClick={toggleFoldDerived} - /> - } - title='Анимация вращения' - disabled={!is3D} - onClick={toggleOrbit} - /> - {controller.isContentEditable ? ( - } - disabled={controller.isProcessing} - onClick={onCreate} - /> - ) : null} - {controller.isContentEditable ? ( - } - disabled={controller.nothingSelected || controller.isProcessing} - onClick={onDelete} - /> - ) : null} - } - title='Сохранить изображение' - onClick={onSaveImage} - /> - -
- isBasicConcept(cst.cst_type)).map(cst => cst.id)} - setSelected={controller.setSelected} +
+ } + onClick={showParamsDialog} /> - + } + title='Граф целиком' + onClick={onResetViewpoint} + /> + + ) : ( + + ) + } + onClick={toggleNoText} + /> + + ) : ( + + ) + } + onClick={toggleFoldDerived} + /> + } + title='Анимация вращения' + disabled={!is3D} + onClick={toggleOrbit} + /> + {controller.isContentEditable ? ( + } + disabled={controller.isProcessing} + onClick={onCreate} + /> + ) : null} + {controller.isContentEditable ? ( + } + disabled={controller.nothingSelected || controller.isProcessing} + onClick={onDelete} + /> + ) : null} + } + title='Сохранить изображение' + onClick={onSaveImage} + /> + +
); } diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/TermGraph.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/TermGraph.tsx index cb73d606..9b0887e1 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/TermGraph.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/TermGraph.tsx @@ -20,6 +20,7 @@ interface TermGraphProps { setHoverID: (newID: ConstituentaID | undefined) => void; onEdit: (cstID: ConstituentaID) => void; + onSelectCentral: (selectedID: ConstituentaID) => void; onSelect: (newID: ConstituentaID) => void; onDeselect: (newID: ConstituentaID) => void; @@ -37,9 +38,11 @@ function TermGraph({ toggleResetView, setHoverID, onEdit, + onSelectCentral, onSelect, onDeselect }: TermGraphProps) { + let ctrlKey: boolean = false; const { calculateHeight, darkMode } = useConceptOptions(); const { selections, setSelections } = useSelection({ @@ -63,13 +66,15 @@ function TermGraph({ const handleNodeClick = useCallback( (node: GraphNode) => { - if (selections.includes(node.id)) { + if (ctrlKey) { + onSelectCentral(Number(node.id)); + } else if (selections.includes(node.id)) { onDeselect(Number(node.id)); } else { onSelect(Number(node.id)); } }, - [onSelect, selections, onDeselect] + [onSelect, selections, onDeselect, onSelectCentral, ctrlKey] ); const handleNodeDoubleClick = useCallback( @@ -96,7 +101,13 @@ function TermGraph({ const canvasHeight = useMemo(() => calculateHeight('1.75rem + 4px'), [calculateHeight]); return ( -
+
(ctrlKey = event.ctrlKey)} + onKeyDown={event => (ctrlKey = event.ctrlKey)} + >
void; + setFocus: (cstID: ConstituentaID) => void; onEdit: (cstID: ConstituentaID) => void; } -function ViewHidden({ items, selected, toggleSelection, schema, coloringScheme, onEdit }: ViewHiddenProps) { +function ViewHidden({ items, selected, toggleSelection, setFocus, schema, coloringScheme, onEdit }: ViewHiddenProps) { const { colors, calculateHeight } = useConceptOptions(); const windowSize = useWindowSize(); const localSelected = useMemo(() => items.filter(id => selected.includes(id)), [items, selected]); const [isFolded, setIsFolded] = useLocalStorage(storage.rsgraphFoldHidden, false); + const handleClick = useCallback( + (cstID: ConstituentaID, event: CProps.EventMouse) => { + if (event.ctrlKey) { + setFocus(cstID); + } else { + toggleSelection(cstID); + } + }, + [setFocus, toggleSelection] + ); + if (!schema || items.length <= 0) { return null; } @@ -93,7 +106,7 @@ function ViewHidden({ items, selected, toggleSelection, schema, coloringScheme, backgroundColor: colorBgGraphNode(cst, adjustedColoring, colors), ...(localSelected.includes(cstID) ? { outlineWidth: '2px', outlineStyle: 'solid' } : {}) }} - onClick={() => toggleSelection(cstID)} + onClick={event => handleClick(cstID, event)} onDoubleClick={() => onEdit(cstID)} > {cst.alias} diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/useGraphFilter.ts b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/useGraphFilter.ts index 7de5ad13..91870e2c 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/useGraphFilter.ts +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/useGraphFilter.ts @@ -2,9 +2,9 @@ import { useLayoutEffect, useMemo, useState } from 'react'; import { Graph } from '@/models/Graph'; import { GraphFilterParams } from '@/models/miscellaneous'; -import { CstType, IRSForm } from '@/models/rsform'; +import { ConstituentaID, CstType, IConstituenta, IRSForm } from '@/models/rsform'; -function useGraphFilter(schema: IRSForm | undefined, params: GraphFilterParams) { +function useGraphFilter(schema: IRSForm | undefined, params: GraphFilterParams, focusCst: IConstituenta | undefined) { const [filtered, setFiltered] = useState(new Graph()); const allowedTypes: CstType[] = useMemo(() => { @@ -29,32 +29,45 @@ function useGraphFilter(schema: IRSForm | undefined, params: GraphFilterParams) if (params.noHermits) { graph.removeIsolated(); } - if (params.noTransitive) { - graph.transitiveReduction(); - } if (params.noTemplates) { schema.items.forEach(cst => { - if (cst.is_template) { + if (cst !== focusCst && cst.is_template) { graph.foldNode(cst.id); } }); } if (allowedTypes.length < Object.values(CstType).length) { schema.items.forEach(cst => { - if (!allowedTypes.includes(cst.cst_type)) { + if (cst !== focusCst && !allowedTypes.includes(cst.cst_type)) { graph.foldNode(cst.id); } }); } - if (params.foldDerived) { + if (!focusCst && params.foldDerived) { schema.items.forEach(cst => { - if (cst.parent_alias) { + if (cst.parent) { graph.foldNode(cst.id); } }); } + if (focusCst) { + const includes: ConstituentaID[] = [ + focusCst.id, + ...focusCst.children, + ...(params.focusShowInputs ? schema.graph.expandInputs([focusCst.id]) : []), + ...(params.focusShowOutputs ? schema.graph.expandOutputs([focusCst.id]) : []) + ]; + schema.items.forEach(cst => { + if (!includes.includes(cst.id)) { + graph.foldNode(cst.id); + } + }); + } + if (params.noTransitive) { + graph.transitiveReduction(); + } setFiltered(graph); - }, [schema, params, allowedTypes]); + }, [schema, params, allowedTypes, focusCst]); return filtered; } diff --git a/rsconcept/frontend/src/pages/RSFormPage/RSTabs.tsx b/rsconcept/frontend/src/pages/RSFormPage/RSTabs.tsx index a165a401..3bfc9e36 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/RSTabs.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/RSTabs.tsx @@ -145,11 +145,11 @@ function RSTabs() { const onOpenCst = useCallback( (cstID: ConstituentaID) => { - if (cstID !== activeCst?.id) { + if (cstID !== activeCst?.id || activeTab !== RSTabID.CST_EDIT) { navigateTab(RSTabID.CST_EDIT, cstID); } }, - [navigateTab, activeCst] + [navigateTab, activeCst, activeTab] ); const onDestroySchema = useCallback(() => { diff --git a/rsconcept/frontend/src/utils/constants.ts b/rsconcept/frontend/src/utils/constants.ts index 36750dba..22080ede 100644 --- a/rsconcept/frontend/src/utils/constants.ts +++ b/rsconcept/frontend/src/utils/constants.ts @@ -90,7 +90,7 @@ export const storage = { librarySearchStrategy: 'library.search.strategy', libraryPagination: 'library.pagination', - rsgraphFilter: 'rsgraph.filter_options', + rsgraphFilter: 'rsgraph.filter2', rsgraphLayout: 'rsgraph.layout', rsgraphColoring: 'rsgraph.coloring', rsgraphSizing: 'rsgraph.sizing',