mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-08-14 12:50:37 +03:00
F: Implement association graph UI
This commit is contained in:
parent
87c7e443e5
commit
bd6f72aceb
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
@ -218,6 +218,7 @@
|
||||||
"Никанорова",
|
"Никанорова",
|
||||||
"Номеноид",
|
"Номеноид",
|
||||||
"номеноида",
|
"номеноида",
|
||||||
|
"номеноидом",
|
||||||
"Номеноиды",
|
"Номеноиды",
|
||||||
"операционализации",
|
"операционализации",
|
||||||
"операционализированных",
|
"операционализированных",
|
||||||
|
|
|
@ -161,7 +161,6 @@ export { BiGitBranch as IconGraphInputs } from 'react-icons/bi';
|
||||||
export { TbEarScan as IconGraphInverse } from 'react-icons/tb';
|
export { TbEarScan as IconGraphInverse } from 'react-icons/tb';
|
||||||
export { BiGitMerge as IconGraphOutputs } from 'react-icons/bi';
|
export { BiGitMerge as IconGraphOutputs } from 'react-icons/bi';
|
||||||
export { LuAtom as IconGraphCore } from 'react-icons/lu';
|
export { LuAtom as IconGraphCore } from 'react-icons/lu';
|
||||||
export { LuRotate3D as IconRotate3D } from 'react-icons/lu';
|
|
||||||
export { MdOutlineFitScreen as IconFitImage } from 'react-icons/md';
|
export { MdOutlineFitScreen as IconFitImage } from 'react-icons/md';
|
||||||
export { RiFocus3Line as IconFocus } from 'react-icons/ri';
|
export { RiFocus3Line as IconFocus } from 'react-icons/ri';
|
||||||
export { LuSparkles as IconClustering } from 'react-icons/lu';
|
export { LuSparkles as IconClustering } from 'react-icons/lu';
|
||||||
|
|
|
@ -6,9 +6,14 @@ export function HelpConceptRelations() {
|
||||||
<div className='text-justify'>
|
<div className='text-justify'>
|
||||||
<h1>Связи между конституентами</h1>
|
<h1>Связи между конституентами</h1>
|
||||||
<p>
|
<p>
|
||||||
Конституенты связаны между собой через использование одних конституент при определении других. Такую связь в
|
Наиболее общей связью между конституентами является ассоциация, устанавливаемая между номеноидом и относимыми к
|
||||||
общем случае называют <b>используется в определении</b>. Она является основой для построения <b>Графа термов</b>
|
нему другими конституентами. Такая связь задается до установления точных определений и применяется для
|
||||||
, отображающего последовательность вывода понятий в концептуальной схеме.
|
предварительной фиксации групп связанных конституент.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Конституенты также связаны между собой через использование одних конституент при определении других. Такую связь
|
||||||
|
в общем случае называют <b>используется в определении</b>. Она является основой для построения{' '}
|
||||||
|
<b>Графа термов</b>, отображающего последовательность вывода понятий в концептуальной схеме.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
|
|
@ -20,7 +20,6 @@ import {
|
||||||
IconOSS,
|
IconOSS,
|
||||||
IconPredecessor,
|
IconPredecessor,
|
||||||
IconReset,
|
IconReset,
|
||||||
IconRotate3D,
|
|
||||||
IconText,
|
IconText,
|
||||||
IconTypeGraph
|
IconTypeGraph
|
||||||
} from '@/components/icons';
|
} from '@/components/icons';
|
||||||
|
@ -37,15 +36,15 @@ export function HelpRSGraphTerm() {
|
||||||
<h2>Настройка графа</h2>
|
<h2>Настройка графа</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Цвет – покраска узлов</li>
|
<li>Цвет – покраска узлов</li>
|
||||||
|
<li>
|
||||||
|
Связи – выбор типов <LinkTopic text='связей' topic={HelpTopic.CC_RELATIONS} />
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconText className='inline-icon' /> Отображение текста
|
<IconText className='inline-icon' /> Отображение текста
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconClustering className='inline-icon' /> Скрыть порожденные
|
<IconClustering className='inline-icon' /> Скрыть порожденные
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<IconRotate3D className='inline-icon' /> Вращение 3D
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -7,11 +7,14 @@ import { type IConstituenta, type IRSForm } from '@/features/rsform';
|
||||||
import { TGEdgeTypes } from '@/features/rsform/components/term-graph/graph/tg-edge-types';
|
import { TGEdgeTypes } from '@/features/rsform/components/term-graph/graph/tg-edge-types';
|
||||||
import { TGNodeTypes } from '@/features/rsform/components/term-graph/graph/tg-node-types';
|
import { TGNodeTypes } from '@/features/rsform/components/term-graph/graph/tg-node-types';
|
||||||
import { SelectColoring } from '@/features/rsform/components/term-graph/select-coloring';
|
import { SelectColoring } from '@/features/rsform/components/term-graph/select-coloring';
|
||||||
|
import { SelectGraphType } from '@/features/rsform/components/term-graph/select-graph-type';
|
||||||
import { ToolbarFocusedCst } from '@/features/rsform/components/term-graph/toolbar-focused-cst';
|
import { ToolbarFocusedCst } from '@/features/rsform/components/term-graph/toolbar-focused-cst';
|
||||||
|
import { ViewHidden } from '@/features/rsform/components/term-graph/view-hidden';
|
||||||
import { applyLayout, produceFilteredGraph, type TGNodeData } from '@/features/rsform/models/graph-api';
|
import { applyLayout, produceFilteredGraph, type TGNodeData } from '@/features/rsform/models/graph-api';
|
||||||
import { useTermGraphStore } from '@/features/rsform/stores/term-graph';
|
import { useTermGraphStore } from '@/features/rsform/stores/term-graph';
|
||||||
|
|
||||||
import { DiagramFlow, useReactFlow } from '@/components/flow/diagram-flow';
|
import { DiagramFlow, useReactFlow } from '@/components/flow/diagram-flow';
|
||||||
|
import { useFitHeight } from '@/stores/app-layout';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
|
|
||||||
import ToolbarGraphFilter from './toolbar-graph-filter';
|
import ToolbarGraphFilter from './toolbar-graph-filter';
|
||||||
|
@ -35,6 +38,8 @@ export function TGReadonlyFlow({ schema }: TGReadonlyFlowProps) {
|
||||||
|
|
||||||
const filter = useTermGraphStore(state => state.filter);
|
const filter = useTermGraphStore(state => state.filter);
|
||||||
const filteredGraph = produceFilteredGraph(schema, filter, focusCst);
|
const filteredGraph = produceFilteredGraph(schema, filter, focusCst);
|
||||||
|
const hidden = schema.items.filter(cst => !filteredGraph.hasNode(cst.id)).map(cst => cst.id);
|
||||||
|
const hiddenHeight = useFitHeight('15.5rem + 2px', '4rem');
|
||||||
|
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]);
|
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]);
|
||||||
const [edges, setEdges] = useEdgesState<Edge>([]);
|
const [edges, setEdges] = useEdgesState<Edge>([]);
|
||||||
|
@ -102,7 +107,9 @@ export function TGReadonlyFlow({ schema }: TGReadonlyFlowProps) {
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className='absolute z-pop top-24 sm:top-16 left-2 sm:left-3 w-54 flex flex-col pointer-events-none'>
|
<div className='absolute z-pop top-24 sm:top-16 left-2 sm:left-3 w-54 flex flex-col pointer-events-none'>
|
||||||
<SelectColoring schema={schema} />
|
<SelectColoring className='rounded-b-none' schema={schema} />
|
||||||
|
<SelectGraphType className='rounded-none border-t-0' />
|
||||||
|
<ViewHidden items={hidden} listHeight={hiddenHeight} schema={schema} setFocus={setFocusCst} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DiagramFlow
|
<DiagramFlow
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { TokenID } from './backend/types';
|
||||||
import { CstClass, ExpressionStatus, type IConstituenta } from './models/rsform';
|
import { CstClass, ExpressionStatus, type IConstituenta } from './models/rsform';
|
||||||
import { type ISyntaxTreeNode } from './models/rslang';
|
import { type ISyntaxTreeNode } from './models/rslang';
|
||||||
import { type TypificationGraphNode } from './models/typification-graph';
|
import { type TypificationGraphNode } from './models/typification-graph';
|
||||||
import { type GraphColoring } from './stores/term-graph';
|
import { type GraphColoring, type GraphType } from './stores/term-graph';
|
||||||
|
|
||||||
/** Represents Brackets highlights theme. */
|
/** Represents Brackets highlights theme. */
|
||||||
export const BRACKETS_THEME = {
|
export const BRACKETS_THEME = {
|
||||||
|
@ -252,3 +252,14 @@ export function colorBgTMGraphNode(node: TypificationGraphNode): string {
|
||||||
}
|
}
|
||||||
return APP_COLORS.bgOrange;
|
return APP_COLORS.bgOrange;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function colorGraphEdge(edgeType: GraphType): string {
|
||||||
|
switch (edgeType) {
|
||||||
|
case 'full':
|
||||||
|
return APP_COLORS.bgGreen;
|
||||||
|
case 'definition':
|
||||||
|
return APP_COLORS.border;
|
||||||
|
case 'association':
|
||||||
|
return APP_COLORS.bgPurple;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -20,9 +20,7 @@ export function SelectColoring({ className, schema }: SelectColoringProps) {
|
||||||
const setColoring = useTermGraphStore(state => state.setColoring);
|
const setColoring = useTermGraphStore(state => state.setColoring);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={cn('relative select-none bg-input border pointer-events-auto', className)}>
|
||||||
className={cn('relative border rounded-b-none select-none bg-input rounded-t-md pointer-events-auto', className)}
|
|
||||||
>
|
|
||||||
<div className='absolute z-pop right-10 h-9 flex items-center'>
|
<div className='absolute z-pop right-10 h-9 flex items-center'>
|
||||||
{coloring === 'status' ? <BadgeHelp topic={HelpTopic.UI_CST_STATUS} contentClass='min-w-100' /> : null}
|
{coloring === 'status' ? <BadgeHelp topic={HelpTopic.UI_CST_STATUS} contentClass='min-w-100' /> : null}
|
||||||
{coloring === 'type' ? <BadgeHelp topic={HelpTopic.UI_CST_CLASS} contentClass='min-w-100' /> : null}
|
{coloring === 'type' ? <BadgeHelp topic={HelpTopic.UI_CST_CLASS} contentClass='min-w-100' /> : null}
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/input/select';
|
||||||
|
import { cn } from '@/components/utils';
|
||||||
|
|
||||||
|
import { labelGraphType } from '../../labels';
|
||||||
|
import { graphTypes, useTermGraphStore } from '../../stores/term-graph';
|
||||||
|
|
||||||
|
interface SelectGraphTypeProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SelectGraphType({ className }: SelectGraphTypeProps) {
|
||||||
|
const graphType = useTermGraphStore(state => state.filter.graphType);
|
||||||
|
const setGraphType = useTermGraphStore(state => state.setGraphType);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('relative border select-none bg-input pointer-events-auto', className)}>
|
||||||
|
<Select onValueChange={setGraphType} defaultValue={graphType}>
|
||||||
|
<SelectTrigger noBorder className='w-full'>
|
||||||
|
<SelectValue placeholder='Цветовая схема' />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent alignOffset={-1} sideOffset={-4}>
|
||||||
|
{graphTypes.map(mode => (
|
||||||
|
<SelectItem key={`graphType-${mode}`} value={mode}>
|
||||||
|
{labelGraphType(mode)}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -4,35 +4,44 @@ import clsx from 'clsx';
|
||||||
|
|
||||||
import { MiniButton } from '@/components/control';
|
import { MiniButton } from '@/components/control';
|
||||||
import { IconDropArrow, IconDropArrowUp } from '@/components/icons';
|
import { IconDropArrow, IconDropArrowUp } from '@/components/icons';
|
||||||
import { useWindowSize } from '@/hooks/use-window-size';
|
|
||||||
import { useFitHeight } from '@/stores/app-layout';
|
|
||||||
import { globalIDs, prefixes } from '@/utils/constants';
|
import { globalIDs, prefixes } from '@/utils/constants';
|
||||||
|
|
||||||
import { colorBgGraphNode } from '../../../colors';
|
import { colorBgGraphNode } from '../../colors';
|
||||||
import { type IConstituenta } from '../../../models/rsform';
|
import { type IConstituenta, type IRSForm } from '../../models/rsform';
|
||||||
import { useCstTooltipStore } from '../../../stores/cst-tooltip';
|
import { useCstTooltipStore } from '../../stores/cst-tooltip';
|
||||||
import { useTermGraphStore } from '../../../stores/term-graph';
|
import { useTermGraphStore } from '../../stores/term-graph';
|
||||||
import { useRSEdit } from '../rsedit-context';
|
|
||||||
|
|
||||||
interface ViewHiddenProps {
|
interface ViewHiddenProps {
|
||||||
items: number[];
|
items: number[];
|
||||||
|
listHeight?: string;
|
||||||
|
|
||||||
|
schema: IRSForm;
|
||||||
|
selected?: number[];
|
||||||
|
toggleSelect?: (id: number) => void;
|
||||||
|
setFocus: (cst: IConstituenta) => void;
|
||||||
|
onActivate?: (id: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ViewHidden({ items }: ViewHiddenProps) {
|
export function ViewHidden({
|
||||||
const { isSmall } = useWindowSize();
|
items,
|
||||||
|
listHeight,
|
||||||
|
schema,
|
||||||
|
selected,
|
||||||
|
toggleSelect,
|
||||||
|
setFocus,
|
||||||
|
onActivate
|
||||||
|
}: ViewHiddenProps) {
|
||||||
const coloring = useTermGraphStore(state => state.coloring);
|
const coloring = useTermGraphStore(state => state.coloring);
|
||||||
const { navigateCst, setFocus, schema, selected, toggleSelect } = useRSEdit();
|
|
||||||
|
|
||||||
const localSelected = items.filter(id => selected.includes(id));
|
const localSelected = selected ? items.filter(id => selected.includes(id)) : [];
|
||||||
const isFolded = useTermGraphStore(state => state.foldHidden);
|
const isFolded = useTermGraphStore(state => state.foldHidden);
|
||||||
const toggleFolded = useTermGraphStore(state => state.toggleFoldHidden);
|
const toggleFolded = useTermGraphStore(state => state.toggleFoldHidden);
|
||||||
const setActiveCst = useCstTooltipStore(state => state.setActiveCst);
|
const setActiveCst = useCstTooltipStore(state => state.setActiveCst);
|
||||||
const hiddenHeight = useFitHeight(isSmall ? '10.4rem + 2px' : '12.5rem + 2px');
|
|
||||||
|
|
||||||
function handleClick(event: React.MouseEvent<Element>, cstID: number) {
|
function handleClick(event: React.MouseEvent<Element>, cstID: number) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
toggleSelect(cstID);
|
toggleSelect?.(cstID);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleContextMenu(event: React.MouseEvent<HTMLElement>, target: IConstituenta) {
|
function handleContextMenu(event: React.MouseEvent<HTMLElement>, target: IConstituenta) {
|
||||||
|
@ -56,7 +65,7 @@ export function ViewHidden({ items }: ViewHiddenProps) {
|
||||||
|
|
||||||
<div className={clsx('py-2 bg-input border-x', isFolded && 'border-b rounded-b-md')}>
|
<div className={clsx('py-2 bg-input border-x', isFolded && 'border-b rounded-b-md')}>
|
||||||
<div className={clsx('w-fit select-none cc-view-hidden-header', !isFolded && 'open')}>
|
<div className={clsx('w-fit select-none cc-view-hidden-header', !isFolded && 'open')}>
|
||||||
{`Скрытые [${localSelected.length} | ${items.length}]`}
|
{localSelected ? `Скрытые [${localSelected.length} | ${items.length}]` : 'Скрытые'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -70,7 +79,7 @@ export function ViewHidden({ items }: ViewHiddenProps) {
|
||||||
!isFolded && 'open'
|
!isFolded && 'open'
|
||||||
)}
|
)}
|
||||||
inert={isFolded}
|
inert={isFolded}
|
||||||
style={{ maxHeight: hiddenHeight }}
|
style={{ maxHeight: listHeight }}
|
||||||
>
|
>
|
||||||
{items.map(cstID => {
|
{items.map(cstID => {
|
||||||
const cst = schema.cstByID.get(cstID)!;
|
const cst = schema.cstByID.get(cstID)!;
|
||||||
|
@ -87,7 +96,7 @@ export function ViewHidden({ items }: ViewHiddenProps) {
|
||||||
style={{ backgroundColor: colorBgGraphNode(cst, coloring) }}
|
style={{ backgroundColor: colorBgGraphNode(cst, coloring) }}
|
||||||
onClick={event => handleClick(event, cstID)}
|
onClick={event => handleClick(event, cstID)}
|
||||||
onContextMenu={event => handleContextMenu(event, cst)}
|
onContextMenu={event => handleContextMenu(event, cst)}
|
||||||
onDoubleClick={() => navigateCst(cstID)}
|
onDoubleClick={() => onActivate?.(cstID)}
|
||||||
data-tooltip-id={globalIDs.constituenta_tooltip}
|
data-tooltip-id={globalIDs.constituenta_tooltip}
|
||||||
onMouseEnter={() => setActiveCst(cst)}
|
onMouseEnter={() => setActiveCst(cst)}
|
||||||
>
|
>
|
|
@ -37,6 +37,13 @@ export function DlgGraphParams() {
|
||||||
name='noText'
|
name='noText'
|
||||||
render={({ field }) => <Checkbox {...field} label='Скрыть текст' title='Не отображать термины' />}
|
render={({ field }) => <Checkbox {...field} label='Скрыть текст' title='Не отображать термины' />}
|
||||||
/>
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name='foldDerived'
|
||||||
|
render={({ field }) => (
|
||||||
|
<Checkbox {...field} label='Скрыть порожденные' title='Не отображать порожденные понятия' />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name='noHermits'
|
name='noHermits'
|
||||||
|
@ -64,13 +71,6 @@ export function DlgGraphParams() {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name='foldDerived'
|
|
||||||
render={({ field }) => (
|
|
||||||
<Checkbox {...field} label='Свернуть порожденные' title='Не отображать порожденные понятия' />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-col gap-1'>
|
<div className='flex flex-col gap-1'>
|
||||||
<h1 className='mb-1'>Типы конституент</h1>
|
<h1 className='mb-1'>Типы конституент</h1>
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { Grammeme, ReferenceType } from './models/language';
|
||||||
import { CstClass, ExpressionStatus, type IConstituenta } from './models/rsform';
|
import { CstClass, ExpressionStatus, type IConstituenta } from './models/rsform';
|
||||||
import { type IArgumentInfo, type ISyntaxTreeNode } from './models/rslang';
|
import { type IArgumentInfo, type ISyntaxTreeNode } from './models/rslang';
|
||||||
import { CstMatchMode, DependencyMode } from './stores/cst-search';
|
import { CstMatchMode, DependencyMode } from './stores/cst-search';
|
||||||
import { type GraphColoring } from './stores/term-graph';
|
import { type GraphColoring, type GraphType } from './stores/term-graph';
|
||||||
|
|
||||||
// --- Records for label/describe functions ---
|
// --- Records for label/describe functions ---
|
||||||
const labelCstTypeRecord: Record<CstType, string> = {
|
const labelCstTypeRecord: Record<CstType, string> = {
|
||||||
|
@ -57,6 +57,12 @@ const labelColoringRecord: Record<GraphColoring, string> = {
|
||||||
schemas: 'Цвет: Схемы'
|
schemas: 'Цвет: Схемы'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const labelGraphTypeRecord: Record<GraphType, string> = {
|
||||||
|
full: 'Связи: Все',
|
||||||
|
definition: 'Связи: Определения',
|
||||||
|
association: 'Связи: Ассоциации'
|
||||||
|
};
|
||||||
|
|
||||||
const labelCstMatchModeRecord: Record<CstMatchMode, string> = {
|
const labelCstMatchModeRecord: Record<CstMatchMode, string> = {
|
||||||
[CstMatchMode.ALL]: 'фильтр',
|
[CstMatchMode.ALL]: 'фильтр',
|
||||||
[CstMatchMode.EXPR]: 'выражение',
|
[CstMatchMode.EXPR]: 'выражение',
|
||||||
|
@ -396,6 +402,11 @@ export function labelColoring(mode: GraphColoring): string {
|
||||||
return labelColoringRecord[mode] ?? `UNKNOWN COLORING: ${mode}`;
|
return labelColoringRecord[mode] ?? `UNKNOWN COLORING: ${mode}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Retrieves label for {@link GraphType}. */
|
||||||
|
export function labelGraphType(mode: GraphType): string {
|
||||||
|
return labelGraphTypeRecord[mode] ?? `UNKNOWN GRAPH TYPE: ${mode}`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves label for {@link ExpressionStatus}.
|
* Retrieves label for {@link ExpressionStatus}.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -7,7 +7,7 @@ import dagre from '@dagrejs/dagre';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
|
|
||||||
import { CstType } from '../backend/types';
|
import { CstType } from '../backend/types';
|
||||||
import { type GraphFilterParams } from '../stores/term-graph';
|
import { type GraphFilterParams, type GraphType } from '../stores/term-graph';
|
||||||
|
|
||||||
import { type IConstituenta, type IRSForm } from './rsform';
|
import { type IConstituenta, type IRSForm } from './rsform';
|
||||||
|
|
||||||
|
@ -57,8 +57,27 @@ export function applyLayout(nodes: Node<TGNodeState>[], edges: Edge[], subLabels
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function inferEdgeType(schema: IRSForm, source: number, target: number): GraphType | null {
|
||||||
|
const isDefinition = schema.graph.hasEdge(source, target);
|
||||||
|
const isAssociation = schema.association_graph.hasEdge(source, target);
|
||||||
|
if (!isDefinition && !isAssociation) {
|
||||||
|
return null;
|
||||||
|
} else if (isDefinition && isAssociation) {
|
||||||
|
return 'full';
|
||||||
|
} else if (isDefinition) {
|
||||||
|
return 'definition';
|
||||||
|
} else {
|
||||||
|
return 'association';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function produceFilteredGraph(schema: IRSForm, params: GraphFilterParams, focusCst: IConstituenta | null) {
|
export function produceFilteredGraph(schema: IRSForm, params: GraphFilterParams, focusCst: IConstituenta | null) {
|
||||||
const filtered = schema.graph.clone();
|
const filtered =
|
||||||
|
params.graphType === 'full'
|
||||||
|
? schema.full_graph.clone()
|
||||||
|
: params.graphType === 'association'
|
||||||
|
? schema.association_graph.clone()
|
||||||
|
: schema.graph.clone();
|
||||||
const allowedTypes: CstType[] = (() => {
|
const allowedTypes: CstType[] = (() => {
|
||||||
const result: CstType[] = [];
|
const result: CstType[] = [];
|
||||||
if (params.allowBase) result.push(CstType.BASE);
|
if (params.allowBase) result.push(CstType.BASE);
|
||||||
|
@ -73,9 +92,6 @@ export function produceFilteredGraph(schema: IRSForm, params: GraphFilterParams,
|
||||||
return result;
|
return result;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
if (params.noHermits) {
|
|
||||||
filtered.removeIsolated();
|
|
||||||
}
|
|
||||||
if (params.noTemplates) {
|
if (params.noTemplates) {
|
||||||
schema.items.forEach(cst => {
|
schema.items.forEach(cst => {
|
||||||
if (cst !== focusCst && cst.is_template) {
|
if (cst !== focusCst && cst.is_template) {
|
||||||
|
@ -97,6 +113,9 @@ export function produceFilteredGraph(schema: IRSForm, params: GraphFilterParams,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (params.noHermits) {
|
||||||
|
filtered.removeIsolated();
|
||||||
|
}
|
||||||
if (focusCst) {
|
if (focusCst) {
|
||||||
const includes: number[] = [
|
const includes: number[] = [
|
||||||
focusCst.id,
|
focusCst.id,
|
||||||
|
|
|
@ -4,21 +4,24 @@ import { useEffect, useRef } from 'react';
|
||||||
import { type Edge, MarkerType, type Node, useEdgesState, useNodesState, useOnSelectionChange } from 'reactflow';
|
import { type Edge, MarkerType, type Node, useEdgesState, useNodesState, useOnSelectionChange } from 'reactflow';
|
||||||
|
|
||||||
import { DiagramFlow, useReactFlow } from '@/components/flow/diagram-flow';
|
import { DiagramFlow, useReactFlow } from '@/components/flow/diagram-flow';
|
||||||
import { useMainHeight } from '@/stores/app-layout';
|
import { useWindowSize } from '@/hooks/use-window-size';
|
||||||
|
import { useFitHeight, useMainHeight } from '@/stores/app-layout';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
import { withPreventDefault } from '@/utils/utils';
|
import { withPreventDefault } from '@/utils/utils';
|
||||||
|
|
||||||
import { useMutatingRSForm } from '../../../backend/use-mutating-rsform';
|
import { useMutatingRSForm } from '../../../backend/use-mutating-rsform';
|
||||||
|
import { colorGraphEdge } from '../../../colors';
|
||||||
import { TGEdgeTypes } from '../../../components/term-graph/graph/tg-edge-types';
|
import { TGEdgeTypes } from '../../../components/term-graph/graph/tg-edge-types';
|
||||||
import { TGNodeTypes } from '../../../components/term-graph/graph/tg-node-types';
|
import { TGNodeTypes } from '../../../components/term-graph/graph/tg-node-types';
|
||||||
import { SelectColoring } from '../../../components/term-graph/select-coloring';
|
import { SelectColoring } from '../../../components/term-graph/select-coloring';
|
||||||
import { applyLayout, type TGNodeData } from '../../../models/graph-api';
|
import { SelectGraphType } from '../../../components/term-graph/select-graph-type';
|
||||||
|
import { ViewHidden } from '../../../components/term-graph/view-hidden';
|
||||||
|
import { applyLayout, inferEdgeType, type TGNodeData } from '../../../models/graph-api';
|
||||||
import { useTermGraphStore } from '../../../stores/term-graph';
|
import { useTermGraphStore } from '../../../stores/term-graph';
|
||||||
import { useRSEdit } from '../rsedit-context';
|
import { useRSEdit } from '../rsedit-context';
|
||||||
|
|
||||||
import { ToolbarTermGraph } from './toolbar-term-graph';
|
import { ToolbarTermGraph } from './toolbar-term-graph';
|
||||||
import { useFilteredGraph } from './use-filtered-graph';
|
import { useFilteredGraph } from './use-filtered-graph';
|
||||||
import { ViewHidden } from './view-hidden';
|
|
||||||
|
|
||||||
export const flowOptions = {
|
export const flowOptions = {
|
||||||
fitView: true,
|
fitView: true,
|
||||||
|
@ -31,17 +34,28 @@ export const flowOptions = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export function TGFlow() {
|
export function TGFlow() {
|
||||||
|
const { isSmall } = useWindowSize();
|
||||||
const mainHeight = useMainHeight();
|
const mainHeight = useMainHeight();
|
||||||
const { fitView, viewportInitialized } = useReactFlow();
|
const { fitView, viewportInitialized } = useReactFlow();
|
||||||
const isProcessing = useMutatingRSForm();
|
const isProcessing = useMutatingRSForm();
|
||||||
const { isContentEditable, schema, selected, setSelected, promptDeleteCst, focusCst, setFocus, navigateCst } =
|
const {
|
||||||
useRSEdit();
|
isContentEditable,
|
||||||
|
schema,
|
||||||
|
selected,
|
||||||
|
setSelected,
|
||||||
|
promptDeleteCst,
|
||||||
|
focusCst,
|
||||||
|
setFocus,
|
||||||
|
toggleSelect,
|
||||||
|
navigateCst
|
||||||
|
} = useRSEdit();
|
||||||
|
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]);
|
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]);
|
||||||
const [edges, setEdges] = useEdgesState<Edge>([]);
|
const [edges, setEdges] = useEdgesState<Edge>([]);
|
||||||
|
|
||||||
const filter = useTermGraphStore(state => state.filter);
|
const filter = useTermGraphStore(state => state.filter);
|
||||||
const { filteredGraph, hidden } = useFilteredGraph();
|
const { filteredGraph, hidden } = useFilteredGraph();
|
||||||
|
const hiddenHeight = useFitHeight(isSmall ? '15rem + 2px' : '13.5rem + 2px', '4rem');
|
||||||
|
|
||||||
function onSelectionChange({ nodes }: { nodes: Node[] }) {
|
function onSelectionChange({ nodes }: { nodes: Node[] }) {
|
||||||
const ids = nodes.map(node => Number(node.id));
|
const ids = nodes.map(node => Number(node.id));
|
||||||
|
@ -73,17 +87,21 @@ export function TGFlow() {
|
||||||
let edgeID = 1;
|
let edgeID = 1;
|
||||||
filteredGraph.nodes.forEach(source => {
|
filteredGraph.nodes.forEach(source => {
|
||||||
source.outputs.forEach(target => {
|
source.outputs.forEach(target => {
|
||||||
if (newNodes.find(node => node.id === String(target))) {
|
const edgeType = inferEdgeType(schema, source.id, target);
|
||||||
|
if (edgeType && newNodes.find(node => node.id === String(target))) {
|
||||||
|
const color = filter.graphType === 'full' ? colorGraphEdge(edgeType) : colorGraphEdge(filter.graphType);
|
||||||
newEdges.push({
|
newEdges.push({
|
||||||
id: String(edgeID),
|
id: String(edgeID),
|
||||||
source: String(source.id),
|
source: String(source.id),
|
||||||
target: String(target),
|
target: String(target),
|
||||||
type: 'termEdge',
|
type: 'termEdge',
|
||||||
|
style: { stroke: color },
|
||||||
focusable: false,
|
focusable: false,
|
||||||
markerEnd: {
|
markerEnd: {
|
||||||
type: MarkerType.ArrowClosed,
|
type: MarkerType.ArrowClosed,
|
||||||
width: 20,
|
width: 20,
|
||||||
height: 20
|
height: 20,
|
||||||
|
color: color
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
edgeID += 1;
|
edgeID += 1;
|
||||||
|
@ -97,7 +115,17 @@ export function TGFlow() {
|
||||||
setEdges(newEdges);
|
setEdges(newEdges);
|
||||||
|
|
||||||
setTimeout(() => fitView(flowOptions.fitViewOptions), PARAMETER.minimalTimeout);
|
setTimeout(() => fitView(flowOptions.fitViewOptions), PARAMETER.minimalTimeout);
|
||||||
}, [schema, filteredGraph, setNodes, setEdges, filter.noText, fitView, viewportInitialized, focusCst]);
|
}, [
|
||||||
|
schema,
|
||||||
|
filteredGraph,
|
||||||
|
setNodes,
|
||||||
|
setEdges,
|
||||||
|
filter.noText,
|
||||||
|
fitView,
|
||||||
|
viewportInitialized,
|
||||||
|
focusCst,
|
||||||
|
filter.graphType
|
||||||
|
]);
|
||||||
|
|
||||||
const prevSelected = useRef<number[]>([]);
|
const prevSelected = useRef<number[]>([]);
|
||||||
if (
|
if (
|
||||||
|
@ -150,8 +178,19 @@ export function TGFlow() {
|
||||||
<span className='px-2 pb-1 select-none whitespace-nowrap backdrop-blur-xs rounded-xl w-fit'>
|
<span className='px-2 pb-1 select-none whitespace-nowrap backdrop-blur-xs rounded-xl w-fit'>
|
||||||
Выбор {selected.length} из {schema.stats?.count_all ?? 0}
|
Выбор {selected.length} из {schema.stats?.count_all ?? 0}
|
||||||
</span>
|
</span>
|
||||||
<SelectColoring schema={schema} />
|
|
||||||
<ViewHidden items={hidden} />
|
<SelectColoring className='rounded-b-none' schema={schema} />
|
||||||
|
<SelectGraphType className='rounded-none border-t-0' />
|
||||||
|
|
||||||
|
<ViewHidden
|
||||||
|
items={hidden}
|
||||||
|
listHeight={hiddenHeight}
|
||||||
|
schema={schema}
|
||||||
|
selected={selected}
|
||||||
|
toggleSelect={toggleSelect}
|
||||||
|
setFocus={setFocus}
|
||||||
|
onActivate={navigateCst}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DiagramFlow
|
<DiagramFlow
|
||||||
|
|
|
@ -156,11 +156,6 @@ export function ToolbarTermGraph({ className }: ToolbarTermGraphProps) {
|
||||||
onClick={toggleClustering}
|
onClick={toggleClustering}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MiniButton
|
|
||||||
icon={<IconTypeGraph size='1.25rem' className='icon-primary' />}
|
|
||||||
title='Граф ступеней'
|
|
||||||
onClick={handleShowTypeGraph}
|
|
||||||
/>
|
|
||||||
<BadgeHelp topic={HelpTopic.UI_GRAPH_TERM} contentClass='sm:max-w-160' offset={4} />
|
<BadgeHelp topic={HelpTopic.UI_GRAPH_TERM} contentClass='sm:max-w-160' offset={4} />
|
||||||
</div>
|
</div>
|
||||||
<div className='cc-icons items-center'>
|
<div className='cc-icons items-center'>
|
||||||
|
@ -198,6 +193,11 @@ export function ToolbarTermGraph({ className }: ToolbarTermGraphProps) {
|
||||||
disabled={!canDeleteSelected || isProcessing}
|
disabled={!canDeleteSelected || isProcessing}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
<MiniButton
|
||||||
|
icon={<IconTypeGraph size='1.25rem' className='icon-primary' />}
|
||||||
|
title='Граф ступеней'
|
||||||
|
onClick={handleShowTypeGraph}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,16 +4,18 @@ import { persist } from 'zustand/middleware';
|
||||||
import { CstType } from '../backend/types';
|
import { CstType } from '../backend/types';
|
||||||
|
|
||||||
export const graphColorings = ['none', 'status', 'type', 'schemas'] as const;
|
export const graphColorings = ['none', 'status', 'type', 'schemas'] as const;
|
||||||
|
export const graphTypes = ['full', 'association', 'definition'] as const;
|
||||||
|
|
||||||
/**
|
/** Represents graph node coloring scheme. */
|
||||||
* Represents graph node coloring scheme.
|
|
||||||
*/
|
|
||||||
export type GraphColoring = (typeof graphColorings)[number];
|
export type GraphColoring = (typeof graphColorings)[number];
|
||||||
|
|
||||||
/**
|
/** Represents graph type. */
|
||||||
* Represents parameters for GraphEditor.
|
export type GraphType = (typeof graphTypes)[number];
|
||||||
*/
|
|
||||||
|
/** Represents parameters for GraphEditor. */
|
||||||
export interface GraphFilterParams {
|
export interface GraphFilterParams {
|
||||||
|
graphType: GraphType;
|
||||||
|
|
||||||
noHermits: boolean;
|
noHermits: boolean;
|
||||||
noTransitive: boolean;
|
noTransitive: boolean;
|
||||||
noTemplates: boolean;
|
noTemplates: boolean;
|
||||||
|
@ -49,10 +51,12 @@ export const cstTypeToFilterKey: Record<CstType, keyof GraphFilterParams> = {
|
||||||
interface TermGraphStore {
|
interface TermGraphStore {
|
||||||
filter: GraphFilterParams;
|
filter: GraphFilterParams;
|
||||||
setFilter: (value: GraphFilterParams) => void;
|
setFilter: (value: GraphFilterParams) => void;
|
||||||
|
setGraphType: (value: GraphType) => void;
|
||||||
toggleFocusInputs: () => void;
|
toggleFocusInputs: () => void;
|
||||||
toggleFocusOutputs: () => void;
|
toggleFocusOutputs: () => void;
|
||||||
toggleText: () => void;
|
toggleText: () => void;
|
||||||
toggleClustering: () => void;
|
toggleClustering: () => void;
|
||||||
|
toggleGraphType: () => void;
|
||||||
|
|
||||||
foldHidden: boolean;
|
foldHidden: boolean;
|
||||||
toggleFoldHidden: () => void;
|
toggleFoldHidden: () => void;
|
||||||
|
@ -65,6 +69,8 @@ export const useTermGraphStore = create<TermGraphStore>()(
|
||||||
persist(
|
persist(
|
||||||
set => ({
|
set => ({
|
||||||
filter: {
|
filter: {
|
||||||
|
graphType: 'full',
|
||||||
|
|
||||||
noTemplates: false,
|
noTemplates: false,
|
||||||
noHermits: true,
|
noHermits: true,
|
||||||
noTransitive: true,
|
noTransitive: true,
|
||||||
|
@ -85,12 +91,25 @@ export const useTermGraphStore = create<TermGraphStore>()(
|
||||||
allowNominal: true
|
allowNominal: true
|
||||||
},
|
},
|
||||||
setFilter: value => set({ filter: value }),
|
setFilter: value => set({ filter: value }),
|
||||||
|
setGraphType: value => set(state => ({ filter: { ...state.filter, graphType: value } })),
|
||||||
toggleFocusInputs: () =>
|
toggleFocusInputs: () =>
|
||||||
set(state => ({ filter: { ...state.filter, focusShowInputs: !state.filter.focusShowInputs } })),
|
set(state => ({ filter: { ...state.filter, focusShowInputs: !state.filter.focusShowInputs } })),
|
||||||
toggleFocusOutputs: () =>
|
toggleFocusOutputs: () =>
|
||||||
set(state => ({ filter: { ...state.filter, focusShowOutputs: !state.filter.focusShowOutputs } })),
|
set(state => ({ filter: { ...state.filter, focusShowOutputs: !state.filter.focusShowOutputs } })),
|
||||||
toggleText: () => set(state => ({ filter: { ...state.filter, noText: !state.filter.noText } })),
|
toggleText: () => set(state => ({ filter: { ...state.filter, noText: !state.filter.noText } })),
|
||||||
toggleClustering: () => set(state => ({ filter: { ...state.filter, foldDerived: !state.filter.foldDerived } })),
|
toggleClustering: () => set(state => ({ filter: { ...state.filter, foldDerived: !state.filter.foldDerived } })),
|
||||||
|
toggleGraphType: () =>
|
||||||
|
set(state => ({
|
||||||
|
filter: {
|
||||||
|
...state.filter,
|
||||||
|
graphType:
|
||||||
|
state.filter.graphType === 'full'
|
||||||
|
? 'association'
|
||||||
|
: state.filter.graphType === 'association'
|
||||||
|
? 'definition'
|
||||||
|
: 'full'
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
|
||||||
foldHidden: false,
|
foldHidden: false,
|
||||||
toggleFoldHidden: () => set(state => ({ foldHidden: !state.foldHidden })),
|
toggleFoldHidden: () => set(state => ({ foldHidden: !state.foldHidden })),
|
||||||
|
@ -99,7 +118,7 @@ export const useTermGraphStore = create<TermGraphStore>()(
|
||||||
setColoring: value => set({ coloring: value })
|
setColoring: value => set({ coloring: value })
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
version: 1,
|
version: 3,
|
||||||
name: 'portal.termGraph'
|
name: 'portal.termGraph'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user