mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
Add focus cst UI
This commit is contained in:
parent
85d756309e
commit
4d72690234
|
@ -6,7 +6,7 @@ function HelpConstituenta() {
|
||||||
return (
|
return (
|
||||||
<div className='dense'>
|
<div className='dense'>
|
||||||
<h1>Редактор конституент</h1>
|
<h1>Редактор конституент</h1>
|
||||||
<p>При выделении также подсвечиваются производные и основание</p>
|
<p>Помимо активной конституенты выделяются порожденные и основание</p>
|
||||||
<p><b>Сохранить изменения</b>: Ctrl + S или клик по кнопке Сохранить</p>
|
<p><b>Сохранить изменения</b>: Ctrl + S или клик по кнопке Сохранить</p>
|
||||||
<p className='mt-1'><b>Формальное определение</b></p>
|
<p className='mt-1'><b>Формальное определение</b></p>
|
||||||
<p>- Ctrl + Пробел дополняет до незанятого имени</p>
|
<p>- Ctrl + Пробел дополняет до незанятого имени</p>
|
||||||
|
|
|
@ -56,8 +56,8 @@ function DlgGraphParams({ hideWindow, initial, onConfirm }: DlgGraphParamsProps)
|
||||||
setValue={value => updateParams({ noTransitive: value })}
|
setValue={value => updateParams({ noTransitive: value })}
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label='Свернуть производные'
|
label='Свернуть порожденные'
|
||||||
title='Не отображать производные понятия'
|
title='Не отображать порожденные понятия'
|
||||||
value={params.foldDerived}
|
value={params.foldDerived}
|
||||||
setValue={value => updateParams({ foldDerived: value })}
|
setValue={value => updateParams({ foldDerived: value })}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -116,6 +116,7 @@ export class Graph {
|
||||||
const result: GraphNode[] = [];
|
const result: GraphNode[] = [];
|
||||||
this.nodes.forEach(node => {
|
this.nodes.forEach(node => {
|
||||||
if (node.outputs.length === 0 && node.inputs.length === 0) {
|
if (node.outputs.length === 0 && node.inputs.length === 0) {
|
||||||
|
result.push(node);
|
||||||
this.nodes.delete(node.id);
|
this.nodes.delete(node.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -103,6 +103,9 @@ export interface GraphFilterParams {
|
||||||
noText: boolean;
|
noText: boolean;
|
||||||
foldDerived: boolean;
|
foldDerived: boolean;
|
||||||
|
|
||||||
|
focusShowInputs: boolean;
|
||||||
|
focusShowOutputs: boolean;
|
||||||
|
|
||||||
allowBase: boolean;
|
allowBase: boolean;
|
||||||
allowStruct: boolean;
|
allowStruct: boolean;
|
||||||
allowTerm: boolean;
|
allowTerm: boolean;
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import InfoConstituenta from '@/components/info/InfoConstituenta';
|
import InfoConstituenta from '@/components/info/InfoConstituenta';
|
||||||
import SelectedCounter from '@/components/info/SelectedCounter';
|
import SelectedCounter from '@/components/info/SelectedCounter';
|
||||||
|
import SelectGraphToolbar from '@/components/select/SelectGraphToolbar';
|
||||||
import { GraphCanvasRef, GraphEdge, GraphLayout, GraphNode } from '@/components/ui/GraphUI';
|
import { GraphCanvasRef, GraphEdge, GraphLayout, GraphNode } from '@/components/ui/GraphUI';
|
||||||
import Overlay from '@/components/ui/Overlay';
|
import Overlay from '@/components/ui/Overlay';
|
||||||
import { useConceptOptions } from '@/context/OptionsContext';
|
import { useConceptOptions } from '@/context/OptionsContext';
|
||||||
|
@ -14,12 +15,14 @@ import DlgGraphParams from '@/dialogs/DlgGraphParams';
|
||||||
import useLocalStorage from '@/hooks/useLocalStorage';
|
import useLocalStorage from '@/hooks/useLocalStorage';
|
||||||
import { GraphColoring, GraphFilterParams, GraphSizing } from '@/models/miscellaneous';
|
import { GraphColoring, GraphFilterParams, GraphSizing } from '@/models/miscellaneous';
|
||||||
import { applyNodeSizing } from '@/models/miscellaneousAPI';
|
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 { colorBgGraphNode } from '@/styling/color';
|
||||||
import { PARAMETER, storage } from '@/utils/constants';
|
import { PARAMETER, storage } from '@/utils/constants';
|
||||||
import { convertBase64ToBlob } from '@/utils/utils';
|
import { convertBase64ToBlob } from '@/utils/utils';
|
||||||
|
|
||||||
import { useRSEdit } from '../RSEditContext';
|
import { useRSEdit } from '../RSEditContext';
|
||||||
|
import FocusToolbar from './FocusToolbar';
|
||||||
import GraphSelectors from './GraphSelectors';
|
import GraphSelectors from './GraphSelectors';
|
||||||
import GraphToolbar from './GraphToolbar';
|
import GraphToolbar from './GraphToolbar';
|
||||||
import TermGraph from './TermGraph';
|
import TermGraph from './TermGraph';
|
||||||
|
@ -41,6 +44,9 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) {
|
||||||
noText: false,
|
noText: false,
|
||||||
foldDerived: false,
|
foldDerived: false,
|
||||||
|
|
||||||
|
focusShowInputs: false,
|
||||||
|
focusShowOutputs: false,
|
||||||
|
|
||||||
allowBase: true,
|
allowBase: true,
|
||||||
allowStruct: true,
|
allowStruct: true,
|
||||||
allowTerm: true,
|
allowTerm: true,
|
||||||
|
@ -51,7 +57,8 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) {
|
||||||
allowTheorem: true
|
allowTheorem: true
|
||||||
});
|
});
|
||||||
const [showParamsDialog, setShowParamsDialog] = useState(false);
|
const [showParamsDialog, setShowParamsDialog] = useState(false);
|
||||||
const filtered = useGraphFilter(controller.schema, filterParams);
|
const [focusCst, setFocusCst] = useState<IConstituenta | undefined>(undefined);
|
||||||
|
const filtered = useGraphFilter(controller.schema, filterParams, focusCst);
|
||||||
|
|
||||||
const graphRef = useRef<GraphCanvasRef | null>(null);
|
const graphRef = useRef<GraphCanvasRef | null>(null);
|
||||||
const [hidden, setHidden] = useState<ConstituentaID[]>([]);
|
const [hidden, setHidden] = useState<ConstituentaID[]>([]);
|
||||||
|
@ -93,7 +100,7 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) {
|
||||||
if (cst) {
|
if (cst) {
|
||||||
result.push({
|
result.push({
|
||||||
id: String(node.id),
|
id: String(node.id),
|
||||||
fill: colorBgGraphNode(cst, coloring, colors),
|
fill: focusCst === cst ? colors.bgPurple : colorBgGraphNode(cst, coloring, colors),
|
||||||
label: cst.alias,
|
label: cst.alias,
|
||||||
subLabel: !filterParams.noText ? cst.term_resolved : undefined,
|
subLabel: !filterParams.noText ? cst.term_resolved : undefined,
|
||||||
size: applyNodeSizing(cst, sizing)
|
size: applyNodeSizing(cst, sizing)
|
||||||
|
@ -101,23 +108,25 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return result;
|
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 edges: GraphEdge[] = useMemo(() => {
|
||||||
const result: GraphEdge[] = [];
|
const result: GraphEdge[] = [];
|
||||||
let edgeID = 1;
|
let edgeID = 1;
|
||||||
filtered.nodes.forEach(source => {
|
filtered.nodes.forEach(source => {
|
||||||
source.outputs.forEach(target => {
|
source.outputs.forEach(target => {
|
||||||
result.push({
|
if (nodes.find(node => node.id === String(target))) {
|
||||||
id: String(edgeID),
|
result.push({
|
||||||
source: String(source.id),
|
id: String(edgeID),
|
||||||
target: String(target)
|
source: String(source.id),
|
||||||
});
|
target: String(target)
|
||||||
edgeID += 1;
|
});
|
||||||
|
edgeID += 1;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
}, [filtered.nodes]);
|
}, [filtered.nodes, nodes]);
|
||||||
|
|
||||||
function handleCreateCst() {
|
function handleCreateCst() {
|
||||||
if (!controller.schema) {
|
if (!controller.schema) {
|
||||||
|
@ -174,6 +183,7 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) {
|
||||||
}
|
}
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
setFocusCst(undefined);
|
||||||
controller.deselectAll();
|
controller.deselectAll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -188,6 +198,17 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) {
|
||||||
}, PARAMETER.graphRefreshDelay);
|
}, PARAMETER.graphRefreshDelay);
|
||||||
}, [setFilterParams, setToggleResetView]);
|
}, [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(
|
const graph = useMemo(
|
||||||
() => (
|
() => (
|
||||||
<TermGraph
|
<TermGraph
|
||||||
|
@ -202,6 +223,7 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) {
|
||||||
onDeselect={controller.deselect}
|
onDeselect={controller.deselect}
|
||||||
setHoverID={setHoverID}
|
setHoverID={setHoverID}
|
||||||
onEdit={onOpenEdit}
|
onEdit={onOpenEdit}
|
||||||
|
onSelectCentral={handleSetFocus}
|
||||||
toggleResetView={toggleResetView}
|
toggleResetView={toggleResetView}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
@ -217,7 +239,8 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) {
|
||||||
onOpenEdit,
|
onOpenEdit,
|
||||||
toggleResetView,
|
toggleResetView,
|
||||||
controller.select,
|
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'
|
position='top-[4.3rem] sm:top-[0.3rem] left-0'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<GraphToolbar
|
<Overlay
|
||||||
is3D={is3D}
|
position='top-0 pt-1 right-1/2 translate-x-1/2'
|
||||||
orbit={orbit}
|
className='flex flex-col items-center rounded-b-2xl cc-blur'
|
||||||
noText={filterParams.noText}
|
>
|
||||||
foldDerived={filterParams.foldDerived}
|
<GraphToolbar
|
||||||
showParamsDialog={() => setShowParamsDialog(true)}
|
is3D={is3D}
|
||||||
onCreate={handleCreateCst}
|
orbit={orbit}
|
||||||
onDelete={handleDeleteCst}
|
noText={filterParams.noText}
|
||||||
onResetViewpoint={() => setToggleResetView(prev => !prev)}
|
foldDerived={filterParams.foldDerived}
|
||||||
onSaveImage={handleSaveImage}
|
showParamsDialog={() => setShowParamsDialog(true)}
|
||||||
toggleOrbit={() => setOrbit(prev => !prev)}
|
onCreate={handleCreateCst}
|
||||||
toggleFoldDerived={handleFoldDerived}
|
onDelete={handleDeleteCst}
|
||||||
toggleNoText={() =>
|
onResetViewpoint={() => setToggleResetView(prev => !prev)}
|
||||||
setFilterParams(prev => ({
|
onSaveImage={handleSaveImage}
|
||||||
...prev,
|
toggleOrbit={() => setOrbit(prev => !prev)}
|
||||||
noText: !prev.noText
|
toggleFoldDerived={handleFoldDerived}
|
||||||
}))
|
toggleNoText={() =>
|
||||||
}
|
setFilterParams(prev => ({
|
||||||
/>
|
...prev,
|
||||||
|
noText: !prev.noText
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{!focusCst ? (
|
||||||
|
<SelectGraphToolbar
|
||||||
|
graph={controller.schema!.graph}
|
||||||
|
core={controller.schema!.items.filter(cst => isBasicConcept(cst.cst_type)).map(cst => cst.id)}
|
||||||
|
setSelected={controller.setSelected}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{focusCst ? (
|
||||||
|
<FocusToolbar
|
||||||
|
center={focusCst}
|
||||||
|
reset={() => handleSetFocus(undefined)}
|
||||||
|
showInputs={filterParams.focusShowInputs}
|
||||||
|
showOutputs={filterParams.focusShowOutputs}
|
||||||
|
toggleShowInputs={() =>
|
||||||
|
setFilterParams(prev => ({
|
||||||
|
...prev,
|
||||||
|
focusShowInputs: !prev.focusShowInputs
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
toggleShowOutputs={() =>
|
||||||
|
setFilterParams(prev => ({
|
||||||
|
...prev,
|
||||||
|
focusShowOutputs: !prev.focusShowOutputs
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</Overlay>
|
||||||
|
|
||||||
{hoverCst ? (
|
{hoverCst ? (
|
||||||
<Overlay
|
<Overlay
|
||||||
|
@ -286,6 +341,7 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) {
|
||||||
schema={controller.schema}
|
schema={controller.schema}
|
||||||
coloringScheme={coloring}
|
coloringScheme={coloring}
|
||||||
toggleSelection={controller.toggleSelect}
|
toggleSelection={controller.toggleSelect}
|
||||||
|
setFocus={handleSetFocus}
|
||||||
onEdit={onOpenEdit}
|
onEdit={onOpenEdit}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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 (
|
||||||
|
<div className='cc-icons items-center'>
|
||||||
|
<div className='w-[7.8rem] text-right select-none' style={{ color: colors.fgPurple }}>
|
||||||
|
Фокус
|
||||||
|
<b className='px-1'> {center.alias} </b>
|
||||||
|
</div>
|
||||||
|
<MiniButton
|
||||||
|
titleHtml='Сбросить фокус'
|
||||||
|
icon={<IconReset size='1.25rem' className='icon-primary' />}
|
||||||
|
onClick={resetSelection}
|
||||||
|
/>
|
||||||
|
<MiniButton
|
||||||
|
title={showInputs ? 'Скрыть поставщиков' : 'Отобразить поставщиков'}
|
||||||
|
icon={
|
||||||
|
showInputs ? (
|
||||||
|
<IconGraphInputs size='1.25rem' className='icon-green' />
|
||||||
|
) : (
|
||||||
|
<IconGraphInputs size='1.25rem' className='icon-primary' />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onClick={toggleShowInputs}
|
||||||
|
/>
|
||||||
|
<MiniButton
|
||||||
|
title={showOutputs ? 'Скрыть потребителей' : 'Отобразить потребителей'}
|
||||||
|
icon={
|
||||||
|
showOutputs ? (
|
||||||
|
<IconGraphOutputs size='1.25rem' className='icon-green' />
|
||||||
|
) : (
|
||||||
|
<IconGraphOutputs size='1.25rem' className='icon-primary' />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onClick={toggleShowOutputs}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FocusToolbar;
|
|
@ -1,5 +1,3 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
IconClustering,
|
IconClustering,
|
||||||
IconClusteringOff,
|
IconClusteringOff,
|
||||||
|
@ -13,11 +11,8 @@ import {
|
||||||
IconTextOff
|
IconTextOff
|
||||||
} from '@/components/Icons';
|
} from '@/components/Icons';
|
||||||
import BadgeHelp from '@/components/man/BadgeHelp';
|
import BadgeHelp from '@/components/man/BadgeHelp';
|
||||||
import SelectGraphToolbar from '@/components/select/SelectGraphToolbar';
|
|
||||||
import MiniButton from '@/components/ui/MiniButton';
|
import MiniButton from '@/components/ui/MiniButton';
|
||||||
import Overlay from '@/components/ui/Overlay';
|
|
||||||
import { HelpTopic } from '@/models/miscellaneous';
|
import { HelpTopic } from '@/models/miscellaneous';
|
||||||
import { isBasicConcept } from '@/models/rsformAPI';
|
|
||||||
|
|
||||||
import { useRSEdit } from '../RSEditContext';
|
import { useRSEdit } from '../RSEditContext';
|
||||||
|
|
||||||
|
@ -56,78 +51,68 @@ function GraphToolbar({
|
||||||
const controller = useRSEdit();
|
const controller = useRSEdit();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Overlay
|
<div className='cc-icons'>
|
||||||
position='top-0 pt-1 right-1/2 translate-x-1/2'
|
<MiniButton
|
||||||
className='flex flex-col items-center rounded-b-2xl cc-blur'
|
title='Настройки фильтрации узлов и связей'
|
||||||
>
|
icon={<IconFilter size='1.25rem' className='icon-primary' />}
|
||||||
<div className='cc-icons'>
|
onClick={showParamsDialog}
|
||||||
<MiniButton
|
|
||||||
title='Настройки фильтрации узлов и связей'
|
|
||||||
icon={<IconFilter size='1.25rem' className='icon-primary' />}
|
|
||||||
onClick={showParamsDialog}
|
|
||||||
/>
|
|
||||||
<MiniButton
|
|
||||||
icon={<IconFitImage size='1.25rem' className='icon-primary' />}
|
|
||||||
title='Граф целиком'
|
|
||||||
onClick={onResetViewpoint}
|
|
||||||
/>
|
|
||||||
<MiniButton
|
|
||||||
title={!noText ? 'Скрыть текст' : 'Отобразить текст'}
|
|
||||||
icon={
|
|
||||||
!noText ? (
|
|
||||||
<IconText size='1.25rem' className='icon-green' />
|
|
||||||
) : (
|
|
||||||
<IconTextOff size='1.25rem' className='icon-primary' />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onClick={toggleNoText}
|
|
||||||
/>
|
|
||||||
<MiniButton
|
|
||||||
title={!foldDerived ? 'Скрыть производные' : 'Отобразить производные'}
|
|
||||||
icon={
|
|
||||||
!foldDerived ? (
|
|
||||||
<IconClustering size='1.25rem' className='icon-green' />
|
|
||||||
) : (
|
|
||||||
<IconClusteringOff size='1.25rem' className='icon-primary' />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onClick={toggleFoldDerived}
|
|
||||||
/>
|
|
||||||
<MiniButton
|
|
||||||
icon={<IconRotate3D size='1.25rem' className={orbit ? 'icon-green' : 'icon-primary'} />}
|
|
||||||
title='Анимация вращения'
|
|
||||||
disabled={!is3D}
|
|
||||||
onClick={toggleOrbit}
|
|
||||||
/>
|
|
||||||
{controller.isContentEditable ? (
|
|
||||||
<MiniButton
|
|
||||||
title='Новая конституента'
|
|
||||||
icon={<IconNewItem size='1.25rem' className='icon-green' />}
|
|
||||||
disabled={controller.isProcessing}
|
|
||||||
onClick={onCreate}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
{controller.isContentEditable ? (
|
|
||||||
<MiniButton
|
|
||||||
title='Удалить выбранные'
|
|
||||||
icon={<IconDestroy size='1.25rem' className='icon-red' />}
|
|
||||||
disabled={controller.nothingSelected || controller.isProcessing}
|
|
||||||
onClick={onDelete}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
<MiniButton
|
|
||||||
icon={<IconImage size='1.25rem' className='icon-primary' />}
|
|
||||||
title='Сохранить изображение'
|
|
||||||
onClick={onSaveImage}
|
|
||||||
/>
|
|
||||||
<BadgeHelp topic={HelpTopic.GRAPH_TERM} className='max-w-[calc(100vw-4rem)]' offset={4} />
|
|
||||||
</div>
|
|
||||||
<SelectGraphToolbar
|
|
||||||
graph={controller.schema!.graph}
|
|
||||||
core={controller.schema!.items.filter(cst => isBasicConcept(cst.cst_type)).map(cst => cst.id)}
|
|
||||||
setSelected={controller.setSelected}
|
|
||||||
/>
|
/>
|
||||||
</Overlay>
|
<MiniButton
|
||||||
|
icon={<IconFitImage size='1.25rem' className='icon-primary' />}
|
||||||
|
title='Граф целиком'
|
||||||
|
onClick={onResetViewpoint}
|
||||||
|
/>
|
||||||
|
<MiniButton
|
||||||
|
title={!noText ? 'Скрыть текст' : 'Отобразить текст'}
|
||||||
|
icon={
|
||||||
|
!noText ? (
|
||||||
|
<IconText size='1.25rem' className='icon-green' />
|
||||||
|
) : (
|
||||||
|
<IconTextOff size='1.25rem' className='icon-primary' />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onClick={toggleNoText}
|
||||||
|
/>
|
||||||
|
<MiniButton
|
||||||
|
title={!foldDerived ? 'Скрыть порожденные' : 'Отобразить порожденные'}
|
||||||
|
icon={
|
||||||
|
!foldDerived ? (
|
||||||
|
<IconClustering size='1.25rem' className='icon-green' />
|
||||||
|
) : (
|
||||||
|
<IconClusteringOff size='1.25rem' className='icon-primary' />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onClick={toggleFoldDerived}
|
||||||
|
/>
|
||||||
|
<MiniButton
|
||||||
|
icon={<IconRotate3D size='1.25rem' className={orbit ? 'icon-green' : 'icon-primary'} />}
|
||||||
|
title='Анимация вращения'
|
||||||
|
disabled={!is3D}
|
||||||
|
onClick={toggleOrbit}
|
||||||
|
/>
|
||||||
|
{controller.isContentEditable ? (
|
||||||
|
<MiniButton
|
||||||
|
title='Новая конституента'
|
||||||
|
icon={<IconNewItem size='1.25rem' className='icon-green' />}
|
||||||
|
disabled={controller.isProcessing}
|
||||||
|
onClick={onCreate}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{controller.isContentEditable ? (
|
||||||
|
<MiniButton
|
||||||
|
title='Удалить выбранные'
|
||||||
|
icon={<IconDestroy size='1.25rem' className='icon-red' />}
|
||||||
|
disabled={controller.nothingSelected || controller.isProcessing}
|
||||||
|
onClick={onDelete}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<MiniButton
|
||||||
|
icon={<IconImage size='1.25rem' className='icon-primary' />}
|
||||||
|
title='Сохранить изображение'
|
||||||
|
onClick={onSaveImage}
|
||||||
|
/>
|
||||||
|
<BadgeHelp topic={HelpTopic.GRAPH_TERM} className='max-w-[calc(100vw-4rem)]' offset={4} />
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ interface TermGraphProps {
|
||||||
|
|
||||||
setHoverID: (newID: ConstituentaID | undefined) => void;
|
setHoverID: (newID: ConstituentaID | undefined) => void;
|
||||||
onEdit: (cstID: ConstituentaID) => void;
|
onEdit: (cstID: ConstituentaID) => void;
|
||||||
|
onSelectCentral: (selectedID: ConstituentaID) => void;
|
||||||
onSelect: (newID: ConstituentaID) => void;
|
onSelect: (newID: ConstituentaID) => void;
|
||||||
onDeselect: (newID: ConstituentaID) => void;
|
onDeselect: (newID: ConstituentaID) => void;
|
||||||
|
|
||||||
|
@ -37,9 +38,11 @@ function TermGraph({
|
||||||
toggleResetView,
|
toggleResetView,
|
||||||
setHoverID,
|
setHoverID,
|
||||||
onEdit,
|
onEdit,
|
||||||
|
onSelectCentral,
|
||||||
onSelect,
|
onSelect,
|
||||||
onDeselect
|
onDeselect
|
||||||
}: TermGraphProps) {
|
}: TermGraphProps) {
|
||||||
|
let ctrlKey: boolean = false;
|
||||||
const { calculateHeight, darkMode } = useConceptOptions();
|
const { calculateHeight, darkMode } = useConceptOptions();
|
||||||
|
|
||||||
const { selections, setSelections } = useSelection({
|
const { selections, setSelections } = useSelection({
|
||||||
|
@ -63,13 +66,15 @@ function TermGraph({
|
||||||
|
|
||||||
const handleNodeClick = useCallback(
|
const handleNodeClick = useCallback(
|
||||||
(node: GraphNode) => {
|
(node: GraphNode) => {
|
||||||
if (selections.includes(node.id)) {
|
if (ctrlKey) {
|
||||||
|
onSelectCentral(Number(node.id));
|
||||||
|
} else if (selections.includes(node.id)) {
|
||||||
onDeselect(Number(node.id));
|
onDeselect(Number(node.id));
|
||||||
} else {
|
} else {
|
||||||
onSelect(Number(node.id));
|
onSelect(Number(node.id));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[onSelect, selections, onDeselect]
|
[onSelect, selections, onDeselect, onSelectCentral, ctrlKey]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleNodeDoubleClick = useCallback(
|
const handleNodeDoubleClick = useCallback(
|
||||||
|
@ -96,7 +101,13 @@ function TermGraph({
|
||||||
const canvasHeight = useMemo(() => calculateHeight('1.75rem + 4px'), [calculateHeight]);
|
const canvasHeight = useMemo(() => calculateHeight('1.75rem + 4px'), [calculateHeight]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='outline-none'>
|
<div
|
||||||
|
className='outline-none'
|
||||||
|
tabIndex={-1}
|
||||||
|
// TODO: fix hacky way of tracking CTRL. Expose event from onNodeClick instead
|
||||||
|
onKeyUp={event => (ctrlKey = event.ctrlKey)}
|
||||||
|
onKeyDown={event => (ctrlKey = event.ctrlKey)}
|
||||||
|
>
|
||||||
<div className='relative' style={{ width: canvasWidth, height: canvasHeight }}>
|
<div className='relative' style={{ width: canvasWidth, height: canvasHeight }}>
|
||||||
<GraphUI
|
<GraphUI
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
|
|
|
@ -2,10 +2,11 @@
|
||||||
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import { IconDropArrow, IconDropArrowUp } from '@/components/Icons';
|
import { IconDropArrow, IconDropArrowUp } from '@/components/Icons';
|
||||||
import ConstituentaTooltip from '@/components/info/ConstituentaTooltip';
|
import ConstituentaTooltip from '@/components/info/ConstituentaTooltip';
|
||||||
|
import { CProps } from '@/components/props';
|
||||||
import MiniButton from '@/components/ui/MiniButton';
|
import MiniButton from '@/components/ui/MiniButton';
|
||||||
import Overlay from '@/components/ui/Overlay';
|
import Overlay from '@/components/ui/Overlay';
|
||||||
import { useConceptOptions } from '@/context/OptionsContext';
|
import { useConceptOptions } from '@/context/OptionsContext';
|
||||||
|
@ -24,15 +25,27 @@ interface ViewHiddenProps {
|
||||||
coloringScheme: GraphColoring;
|
coloringScheme: GraphColoring;
|
||||||
|
|
||||||
toggleSelection: (cstID: ConstituentaID) => void;
|
toggleSelection: (cstID: ConstituentaID) => void;
|
||||||
|
setFocus: (cstID: ConstituentaID) => void;
|
||||||
onEdit: (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 { colors, calculateHeight } = useConceptOptions();
|
||||||
const windowSize = useWindowSize();
|
const windowSize = useWindowSize();
|
||||||
const localSelected = useMemo(() => items.filter(id => selected.includes(id)), [items, selected]);
|
const localSelected = useMemo(() => items.filter(id => selected.includes(id)), [items, selected]);
|
||||||
const [isFolded, setIsFolded] = useLocalStorage(storage.rsgraphFoldHidden, false);
|
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) {
|
if (!schema || items.length <= 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -93,7 +106,7 @@ function ViewHidden({ items, selected, toggleSelection, schema, coloringScheme,
|
||||||
backgroundColor: colorBgGraphNode(cst, adjustedColoring, colors),
|
backgroundColor: colorBgGraphNode(cst, adjustedColoring, colors),
|
||||||
...(localSelected.includes(cstID) ? { outlineWidth: '2px', outlineStyle: 'solid' } : {})
|
...(localSelected.includes(cstID) ? { outlineWidth: '2px', outlineStyle: 'solid' } : {})
|
||||||
}}
|
}}
|
||||||
onClick={() => toggleSelection(cstID)}
|
onClick={event => handleClick(cstID, event)}
|
||||||
onDoubleClick={() => onEdit(cstID)}
|
onDoubleClick={() => onEdit(cstID)}
|
||||||
>
|
>
|
||||||
{cst.alias}
|
{cst.alias}
|
||||||
|
|
|
@ -2,9 +2,9 @@ import { useLayoutEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { Graph } from '@/models/Graph';
|
import { Graph } from '@/models/Graph';
|
||||||
import { GraphFilterParams } from '@/models/miscellaneous';
|
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<Graph>(new Graph());
|
const [filtered, setFiltered] = useState<Graph>(new Graph());
|
||||||
|
|
||||||
const allowedTypes: CstType[] = useMemo(() => {
|
const allowedTypes: CstType[] = useMemo(() => {
|
||||||
|
@ -29,32 +29,45 @@ function useGraphFilter(schema: IRSForm | undefined, params: GraphFilterParams)
|
||||||
if (params.noHermits) {
|
if (params.noHermits) {
|
||||||
graph.removeIsolated();
|
graph.removeIsolated();
|
||||||
}
|
}
|
||||||
if (params.noTransitive) {
|
|
||||||
graph.transitiveReduction();
|
|
||||||
}
|
|
||||||
if (params.noTemplates) {
|
if (params.noTemplates) {
|
||||||
schema.items.forEach(cst => {
|
schema.items.forEach(cst => {
|
||||||
if (cst.is_template) {
|
if (cst !== focusCst && cst.is_template) {
|
||||||
graph.foldNode(cst.id);
|
graph.foldNode(cst.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (allowedTypes.length < Object.values(CstType).length) {
|
if (allowedTypes.length < Object.values(CstType).length) {
|
||||||
schema.items.forEach(cst => {
|
schema.items.forEach(cst => {
|
||||||
if (!allowedTypes.includes(cst.cst_type)) {
|
if (cst !== focusCst && !allowedTypes.includes(cst.cst_type)) {
|
||||||
graph.foldNode(cst.id);
|
graph.foldNode(cst.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (params.foldDerived) {
|
if (!focusCst && params.foldDerived) {
|
||||||
schema.items.forEach(cst => {
|
schema.items.forEach(cst => {
|
||||||
if (cst.parent_alias) {
|
if (cst.parent) {
|
||||||
graph.foldNode(cst.id);
|
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);
|
setFiltered(graph);
|
||||||
}, [schema, params, allowedTypes]);
|
}, [schema, params, allowedTypes, focusCst]);
|
||||||
|
|
||||||
return filtered;
|
return filtered;
|
||||||
}
|
}
|
||||||
|
|
|
@ -145,11 +145,11 @@ function RSTabs() {
|
||||||
|
|
||||||
const onOpenCst = useCallback(
|
const onOpenCst = useCallback(
|
||||||
(cstID: ConstituentaID) => {
|
(cstID: ConstituentaID) => {
|
||||||
if (cstID !== activeCst?.id) {
|
if (cstID !== activeCst?.id || activeTab !== RSTabID.CST_EDIT) {
|
||||||
navigateTab(RSTabID.CST_EDIT, cstID);
|
navigateTab(RSTabID.CST_EDIT, cstID);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[navigateTab, activeCst]
|
[navigateTab, activeCst, activeTab]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onDestroySchema = useCallback(() => {
|
const onDestroySchema = useCallback(() => {
|
||||||
|
|
|
@ -90,7 +90,7 @@ export const storage = {
|
||||||
librarySearchStrategy: 'library.search.strategy',
|
librarySearchStrategy: 'library.search.strategy',
|
||||||
libraryPagination: 'library.pagination',
|
libraryPagination: 'library.pagination',
|
||||||
|
|
||||||
rsgraphFilter: 'rsgraph.filter_options',
|
rsgraphFilter: 'rsgraph.filter2',
|
||||||
rsgraphLayout: 'rsgraph.layout',
|
rsgraphLayout: 'rsgraph.layout',
|
||||||
rsgraphColoring: 'rsgraph.coloring',
|
rsgraphColoring: 'rsgraph.coloring',
|
||||||
rsgraphSizing: 'rsgraph.sizing',
|
rsgraphSizing: 'rsgraph.sizing',
|
||||||
|
|
Loading…
Reference in New Issue
Block a user