F: Term graph rework pt1
This commit is contained in:
parent
80f34d90c4
commit
f6bb76f5e1
|
@ -44,7 +44,6 @@ This readme file is used mostly to document project dependencies and conventions
|
||||||
- js-file-download
|
- js-file-download
|
||||||
- use-debounce
|
- use-debounce
|
||||||
- framer-motion
|
- framer-motion
|
||||||
- reagraph
|
|
||||||
- html-to-image
|
- html-to-image
|
||||||
- @tanstack/react-table
|
- @tanstack/react-table
|
||||||
- @uiw/react-codemirror
|
- @uiw/react-codemirror
|
||||||
|
|
984
rsconcept/frontend/package-lock.json
generated
984
rsconcept/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -36,7 +36,6 @@
|
||||||
"react-tooltip": "^5.28.0",
|
"react-tooltip": "^5.28.0",
|
||||||
"react-zoom-pan-pinch": "^3.6.1",
|
"react-zoom-pan-pinch": "^3.6.1",
|
||||||
"reactflow": "^11.11.4",
|
"reactflow": "^11.11.4",
|
||||||
"reagraph": "^4.20.1",
|
|
||||||
"use-debounce": "^10.0.4"
|
"use-debounce": "^10.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -139,6 +139,7 @@ function PickMultiConstituenta({
|
||||||
graph={foldedGraph}
|
graph={foldedGraph}
|
||||||
isCore={cstID => isBasicConcept(schema.cstByID.get(cstID)?.cst_type)}
|
isCore={cstID => isBasicConcept(schema.cstByID.get(cstID)?.cst_type)}
|
||||||
isOwned={cstID => !schema.cstByID.get(cstID)?.is_inherited}
|
isOwned={cstID => !schema.cstByID.get(cstID)?.is_inherited}
|
||||||
|
selected={selected}
|
||||||
setSelected={setSelected}
|
setSelected={setSelected}
|
||||||
emptySelection={selected.length === 0}
|
emptySelection={selected.length === 0}
|
||||||
className='w-fit'
|
className='w-fit'
|
||||||
|
|
|
@ -19,15 +19,17 @@ import MiniButton from '../ui/MiniButton';
|
||||||
|
|
||||||
interface ToolbarGraphSelectionProps extends CProps.Styling {
|
interface ToolbarGraphSelectionProps extends CProps.Styling {
|
||||||
graph: Graph;
|
graph: Graph;
|
||||||
|
selected: number[];
|
||||||
isCore: (item: number) => boolean;
|
isCore: (item: number) => boolean;
|
||||||
isOwned: (item: number) => boolean;
|
isOwned?: (item: number) => boolean;
|
||||||
setSelected: React.Dispatch<React.SetStateAction<number[]>>;
|
setSelected: (newSelection: number[]) => void;
|
||||||
emptySelection?: boolean;
|
emptySelection?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ToolbarGraphSelection({
|
function ToolbarGraphSelection({
|
||||||
className,
|
className,
|
||||||
graph,
|
graph,
|
||||||
|
selected,
|
||||||
isCore,
|
isCore,
|
||||||
isOwned,
|
isOwned,
|
||||||
setSelected,
|
setSelected,
|
||||||
|
@ -40,13 +42,13 @@ function ToolbarGraphSelection({
|
||||||
}, [setSelected, graph, isCore]);
|
}, [setSelected, graph, isCore]);
|
||||||
|
|
||||||
const handleSelectOwned = useCallback(
|
const handleSelectOwned = useCallback(
|
||||||
() => setSelected([...graph.nodes.keys()].filter(isOwned)),
|
() => (isOwned ? setSelected([...graph.nodes.keys()].filter(isOwned)) : undefined),
|
||||||
[setSelected, graph, isOwned]
|
[setSelected, graph, isOwned]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleInvertSelection = useCallback(
|
const handleInvertSelection = useCallback(
|
||||||
() => setSelected(prev => [...graph.nodes.keys()].filter(item => !prev.includes(item))),
|
() => setSelected([...graph.nodes.keys()].filter(item => !selected.includes(item))),
|
||||||
[setSelected, graph]
|
[setSelected, selected, graph]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -60,31 +62,31 @@ function ToolbarGraphSelection({
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml='Выделить все влияющие'
|
titleHtml='Выделить все влияющие'
|
||||||
icon={<IconGraphCollapse size='1.25rem' className='icon-primary' />}
|
icon={<IconGraphCollapse size='1.25rem' className='icon-primary' />}
|
||||||
onClick={() => setSelected(prev => [...prev, ...graph.expandAllInputs(prev)])}
|
onClick={() => setSelected([...selected, ...graph.expandAllInputs(selected)])}
|
||||||
disabled={emptySelection}
|
disabled={emptySelection}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml='Выделить все зависимые'
|
titleHtml='Выделить все зависимые'
|
||||||
icon={<IconGraphExpand size='1.25rem' className='icon-primary' />}
|
icon={<IconGraphExpand size='1.25rem' className='icon-primary' />}
|
||||||
onClick={() => setSelected(prev => [...prev, ...graph.expandAllOutputs(prev)])}
|
onClick={() => setSelected([...selected, ...graph.expandAllOutputs(selected)])}
|
||||||
disabled={emptySelection}
|
disabled={emptySelection}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml='<b>Максимизация</b> <br/>дополнение выделения конституентами, <br/>зависимыми только от выделенных'
|
titleHtml='<b>Максимизация</b> <br/>дополнение выделения конституентами, <br/>зависимыми только от выделенных'
|
||||||
icon={<IconGraphMaximize size='1.25rem' className='icon-primary' />}
|
icon={<IconGraphMaximize size='1.25rem' className='icon-primary' />}
|
||||||
onClick={() => setSelected(prev => graph.maximizePart(prev))}
|
onClick={() => setSelected(graph.maximizePart(selected))}
|
||||||
disabled={emptySelection}
|
disabled={emptySelection}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml='Выделить поставщиков'
|
titleHtml='Выделить поставщиков'
|
||||||
icon={<IconGraphInputs size='1.25rem' className='icon-primary' />}
|
icon={<IconGraphInputs size='1.25rem' className='icon-primary' />}
|
||||||
onClick={() => setSelected(prev => [...prev, ...graph.expandInputs(prev)])}
|
onClick={() => setSelected([...selected, ...graph.expandInputs(selected)])}
|
||||||
disabled={emptySelection}
|
disabled={emptySelection}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml='Выделить потребителей'
|
titleHtml='Выделить потребителей'
|
||||||
icon={<IconGraphOutputs size='1.25rem' className='icon-primary' />}
|
icon={<IconGraphOutputs size='1.25rem' className='icon-primary' />}
|
||||||
onClick={() => setSelected(prev => [...prev, ...graph.expandOutputs(prev)])}
|
onClick={() => setSelected([...selected, ...graph.expandOutputs(selected)])}
|
||||||
disabled={emptySelection}
|
disabled={emptySelection}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
|
@ -97,11 +99,13 @@ function ToolbarGraphSelection({
|
||||||
icon={<IconGraphCore size='1.25rem' className='icon-primary' />}
|
icon={<IconGraphCore size='1.25rem' className='icon-primary' />}
|
||||||
onClick={handleSelectCore}
|
onClick={handleSelectCore}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
{isOwned ? (
|
||||||
titleHtml='Выделить собственные'
|
<MiniButton
|
||||||
icon={<IconPredecessor size='1.25rem' className='icon-primary' />}
|
titleHtml='Выделить собственные'
|
||||||
onClick={handleSelectOwned}
|
icon={<IconPredecessor size='1.25rem' className='icon-primary' />}
|
||||||
/>
|
onClick={handleSelectOwned}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
// Reexporting necessary reagraph types.
|
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { GraphCanvas as GraphUI } from 'reagraph';
|
|
||||||
|
|
||||||
export {
|
|
||||||
type CollapseProps,
|
|
||||||
type GraphCanvasRef,
|
|
||||||
type GraphEdge,
|
|
||||||
type GraphNode,
|
|
||||||
Sphere,
|
|
||||||
useSelection
|
|
||||||
} from 'reagraph';
|
|
||||||
export { type LayoutTypes as GraphLayout } from 'reagraph';
|
|
||||||
|
|
||||||
import { ThreeEvent } from '@react-three/fiber';
|
|
||||||
|
|
||||||
export type GraphMouseEvent = ThreeEvent<MouseEvent>;
|
|
||||||
export type GraphPointerEvent = ThreeEvent<PointerEvent>;
|
|
||||||
|
|
||||||
export default GraphUI;
|
|
|
@ -57,11 +57,6 @@ export interface MGraphEdgeInternal extends EdgeProps {
|
||||||
*/
|
*/
|
||||||
export type GraphColoring = 'none' | 'status' | 'type' | 'schemas';
|
export type GraphColoring = 'none' | 'status' | 'type' | 'schemas';
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents graph node sizing scheme.
|
|
||||||
*/
|
|
||||||
export type GraphSizing = 'none' | 'complex' | 'derived';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents manuals topic.
|
* Represents manuals topic.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
*/
|
*/
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
|
|
||||||
import { DependencyMode, GraphSizing, Position2D } from './miscellaneous';
|
import { DependencyMode, Position2D } from './miscellaneous';
|
||||||
import { IOperationPosition, IOperationSchema, OperationID, OperationType } from './oss';
|
import { IOperationPosition, IOperationSchema, OperationID, OperationType } from './oss';
|
||||||
import { IConstituenta, IRSForm } from './rsform';
|
import { IConstituenta, IRSForm } from './rsform';
|
||||||
|
|
||||||
|
@ -38,19 +38,6 @@ export function applyGraphFilter(target: IRSForm, start: number, mode: Dependenc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply {@link GraphSizing} to a given {@link IConstituenta}.
|
|
||||||
*/
|
|
||||||
export function applyNodeSizing(target: IConstituenta, sizing: GraphSizing): number | undefined {
|
|
||||||
if (sizing === 'none') {
|
|
||||||
return undefined;
|
|
||||||
} else if (sizing === 'complex') {
|
|
||||||
return target.is_simple_expression ? 1 : 2;
|
|
||||||
} else {
|
|
||||||
return target.spawner ? 1 : 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate insert position for a new {@link IOperation}
|
* Calculate insert position for a new {@link IOperation}
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -32,8 +32,6 @@ function HelpRSGraphTerm() {
|
||||||
<div className='sm:w-[14rem]'>
|
<div className='sm:w-[14rem]'>
|
||||||
<h1>Настройка графа</h1>
|
<h1>Настройка графа</h1>
|
||||||
<li>Цвет – покраска узлов</li>
|
<li>Цвет – покраска узлов</li>
|
||||||
<li>Граф – расположение</li>
|
|
||||||
<li>Размер – размер узлов</li>
|
|
||||||
<li>
|
<li>
|
||||||
<IconText className='inline-icon' /> Отображение текста
|
<IconText className='inline-icon' /> Отображение текста
|
||||||
</li>
|
</li>
|
||||||
|
@ -51,7 +49,7 @@ function HelpRSGraphTerm() {
|
||||||
<h1>Изменение узлов</h1>
|
<h1>Изменение узлов</h1>
|
||||||
<li>Клик на конституенту – выделение</li>
|
<li>Клик на конституенту – выделение</li>
|
||||||
<li>
|
<li>
|
||||||
Ctrl + клик – выбор <span style={{ color: colors.fgPurple }}>фокус-конституенты</span>
|
Alt + клик – выбор <span style={{ color: colors.fgPurple }}>фокус-конституенты</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconReset className='inline-icon' /> Esc – сбросить выделение
|
<IconReset className='inline-icon' /> Esc – сбросить выделение
|
||||||
|
|
|
@ -32,6 +32,9 @@ import { OssNodeTypes } from './graph/OssNodeTypes';
|
||||||
import NodeContextMenu, { ContextMenuData } from './NodeContextMenu';
|
import NodeContextMenu, { ContextMenuData } from './NodeContextMenu';
|
||||||
import ToolbarOssGraph from './ToolbarOssGraph';
|
import ToolbarOssGraph from './ToolbarOssGraph';
|
||||||
|
|
||||||
|
const ZOOM_MAX = 2;
|
||||||
|
const ZOOM_MIN = 0.5;
|
||||||
|
|
||||||
interface OssFlowProps {
|
interface OssFlowProps {
|
||||||
isModified: boolean;
|
isModified: boolean;
|
||||||
setIsModified: (newValue: boolean) => void;
|
setIsModified: (newValue: boolean) => void;
|
||||||
|
@ -223,7 +226,7 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
||||||
const imageWidth = PARAMETER.ossImageWidth;
|
const imageWidth = PARAMETER.ossImageWidth;
|
||||||
const imageHeight = PARAMETER.ossImageHeight;
|
const imageHeight = PARAMETER.ossImageHeight;
|
||||||
const nodesBounds = getNodesBounds(nodes);
|
const nodesBounds = getNodesBounds(nodes);
|
||||||
const viewport = getViewportForBounds(nodesBounds, imageWidth, imageHeight, 0.5, 2);
|
const viewport = getViewportForBounds(nodesBounds, imageWidth, imageHeight, ZOOM_MIN, ZOOM_MAX);
|
||||||
toPng(canvas, {
|
toPng(canvas, {
|
||||||
backgroundColor: colors.bgDefault,
|
backgroundColor: colors.bgDefault,
|
||||||
width: imageWidth,
|
width: imageWidth,
|
||||||
|
@ -266,7 +269,7 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
||||||
setMenuProps(undefined);
|
setMenuProps(undefined);
|
||||||
}, [controller]);
|
}, [controller]);
|
||||||
|
|
||||||
const handleClickCanvas = useCallback(() => {
|
const handleCanvasClick = useCallback(() => {
|
||||||
handleContextMenuHide();
|
handleContextMenuHide();
|
||||||
}, [handleContextMenuHide]);
|
}, [handleContextMenuHide]);
|
||||||
|
|
||||||
|
@ -322,13 +325,13 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
||||||
nodesFocusable={false}
|
nodesFocusable={false}
|
||||||
fitView
|
fitView
|
||||||
nodeTypes={OssNodeTypes}
|
nodeTypes={OssNodeTypes}
|
||||||
maxZoom={2}
|
maxZoom={ZOOM_MAX}
|
||||||
minZoom={0.5}
|
minZoom={ZOOM_MIN}
|
||||||
nodesConnectable={false}
|
nodesConnectable={false}
|
||||||
snapToGrid={true}
|
snapToGrid={true}
|
||||||
snapGrid={[PARAMETER.ossGridSize, PARAMETER.ossGridSize]}
|
snapGrid={[PARAMETER.ossGridSize, PARAMETER.ossGridSize]}
|
||||||
onNodeContextMenu={handleContextMenu}
|
onNodeContextMenu={handleContextMenu}
|
||||||
onClick={handleClickCanvas}
|
onClick={handleCanvasClick}
|
||||||
>
|
>
|
||||||
{showGrid ? <Background gap={PARAMETER.ossGridSize} /> : null}
|
{showGrid ? <Background gap={PARAMETER.ossGridSize} /> : null}
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
|
@ -338,7 +341,7 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
||||||
edges,
|
edges,
|
||||||
handleNodesChange,
|
handleNodesChange,
|
||||||
handleContextMenu,
|
handleContextMenu,
|
||||||
handleClickCanvas,
|
handleCanvasClick,
|
||||||
onEdgesChange,
|
onEdgesChange,
|
||||||
handleNodeDoubleClick,
|
handleNodeDoubleClick,
|
||||||
showGrid
|
showGrid
|
||||||
|
|
|
@ -1,390 +1,18 @@
|
||||||
'use client';
|
import { ReactFlowProvider } from 'reactflow';
|
||||||
|
|
||||||
import clsx from 'clsx';
|
import { ConstituentaID } from '@/models/rsform';
|
||||||
import { AnimatePresence } from 'framer-motion';
|
|
||||||
import fileDownload from 'js-file-download';
|
|
||||||
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
||||||
import { useDebounce } from 'use-debounce';
|
|
||||||
|
|
||||||
import InfoConstituenta from '@/components/info/InfoConstituenta';
|
import TGFlow from './TGFlow';
|
||||||
import SelectedCounter from '@/components/info/SelectedCounter';
|
|
||||||
import ToolbarGraphSelection from '@/components/select/ToolbarGraphSelection';
|
|
||||||
import { GraphCanvasRef, GraphEdge, GraphLayout, GraphNode } from '@/components/ui/GraphUI';
|
|
||||||
import Overlay from '@/components/ui/Overlay';
|
|
||||||
import AnimateFade from '@/components/wrap/AnimateFade';
|
|
||||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
|
||||||
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, 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 GraphSelectors from './GraphSelectors';
|
|
||||||
import TermGraph from './TermGraph';
|
|
||||||
import ToolbarFocusedCst from './ToolbarFocusedCst';
|
|
||||||
import ToolbarTermGraph from './ToolbarTermGraph';
|
|
||||||
import useGraphFilter from './useGraphFilter';
|
|
||||||
import ViewHidden from './ViewHidden';
|
|
||||||
|
|
||||||
interface EditorTermGraphProps {
|
interface EditorTermGraphProps {
|
||||||
onOpenEdit: (cstID: ConstituentaID) => void;
|
onOpenEdit: (cstID: ConstituentaID) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) {
|
function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) {
|
||||||
const controller = useRSEdit();
|
|
||||||
const { colors } = useConceptOptions();
|
|
||||||
|
|
||||||
const [filterParams, setFilterParams] = useLocalStorage<GraphFilterParams>(storage.rsgraphFilter, {
|
|
||||||
noHermits: true,
|
|
||||||
noTemplates: false,
|
|
||||||
noTransitive: true,
|
|
||||||
noText: false,
|
|
||||||
foldDerived: false,
|
|
||||||
|
|
||||||
focusShowInputs: true,
|
|
||||||
focusShowOutputs: true,
|
|
||||||
|
|
||||||
allowBase: true,
|
|
||||||
allowStruct: true,
|
|
||||||
allowTerm: true,
|
|
||||||
allowAxiom: true,
|
|
||||||
allowFunction: true,
|
|
||||||
allowPredicate: true,
|
|
||||||
allowConstant: true,
|
|
||||||
allowTheorem: true
|
|
||||||
});
|
|
||||||
const [showParamsDialog, setShowParamsDialog] = useState(false);
|
|
||||||
const [focusCst, setFocusCst] = useState<IConstituenta | undefined>(undefined);
|
|
||||||
const filtered = useGraphFilter(controller.schema, filterParams, focusCst);
|
|
||||||
|
|
||||||
const graphRef = useRef<GraphCanvasRef | null>(null);
|
|
||||||
const [hidden, setHidden] = useState<ConstituentaID[]>([]);
|
|
||||||
|
|
||||||
const [layout, setLayout] = useLocalStorage<GraphLayout>(storage.rsgraphLayout, 'treeTd2d');
|
|
||||||
const [coloring, setColoring] = useLocalStorage<GraphColoring>(storage.rsgraphColoring, 'type');
|
|
||||||
const [sizing, setSizing] = useLocalStorage<GraphSizing>(storage.rsgraphSizing, 'derived');
|
|
||||||
const [orbit, setOrbit] = useState(false);
|
|
||||||
const is3D = useMemo(() => layout.includes('3d'), [layout]);
|
|
||||||
|
|
||||||
const [hoverID, setHoverID] = useState<ConstituentaID | undefined>(undefined);
|
|
||||||
const hoverCst = useMemo(() => {
|
|
||||||
return hoverID && controller.schema?.cstByID.get(hoverID);
|
|
||||||
}, [controller.schema?.cstByID, hoverID]);
|
|
||||||
const [hoverCstDebounced] = useDebounce(hoverCst, PARAMETER.graphPopupDelay);
|
|
||||||
const [hoverLeft, setHoverLeft] = useState(true);
|
|
||||||
|
|
||||||
const [toggleResetView, setToggleResetView] = useState(false);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
if (!controller.schema) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const newDismissed: ConstituentaID[] = [];
|
|
||||||
controller.schema.items.forEach(cst => {
|
|
||||||
if (!filtered.nodes.has(cst.id)) {
|
|
||||||
newDismissed.push(cst.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setHidden(newDismissed);
|
|
||||||
setHoverID(undefined);
|
|
||||||
}, [controller.schema, filtered]);
|
|
||||||
|
|
||||||
const nodes: GraphNode[] = useMemo(() => {
|
|
||||||
const result: GraphNode[] = [];
|
|
||||||
if (!controller.schema) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
filtered.nodes.forEach(node => {
|
|
||||||
const cst = controller.schema!.cstByID.get(node.id);
|
|
||||||
if (cst) {
|
|
||||||
result.push({
|
|
||||||
id: String(node.id),
|
|
||||||
fill: focusCst === cst ? colors.bgPurple : colorBgGraphNode(cst, coloring, colors),
|
|
||||||
label: `${cst.alias}${cst.is_inherited ? '*' : ''}`,
|
|
||||||
subLabel: !filterParams.noText ? cst.term_resolved : undefined,
|
|
||||||
size: applyNodeSizing(cst, sizing)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}, [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 => {
|
|
||||||
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, nodes]);
|
|
||||||
|
|
||||||
function handleCreateCst() {
|
|
||||||
if (!controller.schema) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const definition = controller.selected.map(id => controller.schema!.cstByID.get(id)!.alias).join(' ');
|
|
||||||
controller.createCst(controller.selected.length === 0 ? CstType.BASE : CstType.TERM, false, definition);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDeleteCst() {
|
|
||||||
if (!controller.schema || !controller.canDeleteSelected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
controller.promptDeleteCst();
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleChangeLayout = useCallback(
|
|
||||||
(newLayout: GraphLayout) => {
|
|
||||||
if (newLayout === layout) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setLayout(newLayout);
|
|
||||||
setTimeout(() => {
|
|
||||||
setToggleResetView(prev => !prev);
|
|
||||||
}, PARAMETER.graphRefreshDelay);
|
|
||||||
},
|
|
||||||
[layout, setLayout]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleChangeParams = useCallback(
|
|
||||||
(params: GraphFilterParams) => {
|
|
||||||
setFilterParams(params);
|
|
||||||
},
|
|
||||||
[setFilterParams]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSaveImage = useCallback(() => {
|
|
||||||
if (!graphRef?.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const data = graphRef.current.exportCanvas();
|
|
||||||
try {
|
|
||||||
fileDownload(convertBase64ToBlob(data), 'graph.png', 'data:image/png;base64');
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}, [graphRef]);
|
|
||||||
|
|
||||||
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
|
||||||
if (controller.isProcessing) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (event.key === 'Escape') {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
setFocusCst(undefined);
|
|
||||||
controller.deselectAll();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!controller.isContentEditable) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (event.key === 'Delete') {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
handleDeleteCst();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleFoldDerived = useCallback(() => {
|
|
||||||
setFilterParams(prev => ({
|
|
||||||
...prev,
|
|
||||||
foldDerived: !prev.foldDerived
|
|
||||||
}));
|
|
||||||
setTimeout(() => {
|
|
||||||
setToggleResetView(prev => !prev);
|
|
||||||
}, 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(
|
|
||||||
() => (
|
|
||||||
<TermGraph
|
|
||||||
graphRef={graphRef}
|
|
||||||
nodes={nodes}
|
|
||||||
edges={edges}
|
|
||||||
selectedIDs={controller.selected}
|
|
||||||
layout={layout}
|
|
||||||
is3D={is3D}
|
|
||||||
orbit={orbit}
|
|
||||||
onSelect={controller.select}
|
|
||||||
onDeselect={controller.deselect}
|
|
||||||
setHoverID={setHoverID}
|
|
||||||
onEdit={onOpenEdit}
|
|
||||||
onSelectCentral={handleSetFocus}
|
|
||||||
toggleResetView={toggleResetView}
|
|
||||||
setHoverLeft={setHoverLeft}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
[
|
|
||||||
graphRef,
|
|
||||||
edges,
|
|
||||||
nodes,
|
|
||||||
controller.selected,
|
|
||||||
layout,
|
|
||||||
is3D,
|
|
||||||
orbit,
|
|
||||||
setHoverID,
|
|
||||||
onOpenEdit,
|
|
||||||
toggleResetView,
|
|
||||||
controller.select,
|
|
||||||
controller.deselect,
|
|
||||||
handleSetFocus
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectors = useMemo(
|
|
||||||
() => (
|
|
||||||
<GraphSelectors
|
|
||||||
schema={controller.schema}
|
|
||||||
coloring={coloring}
|
|
||||||
layout={layout}
|
|
||||||
sizing={sizing}
|
|
||||||
setLayout={handleChangeLayout}
|
|
||||||
setColoring={setColoring}
|
|
||||||
setSizing={setSizing}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
[coloring, controller.schema, layout, sizing, handleChangeLayout, setColoring, setSizing]
|
|
||||||
);
|
|
||||||
const viewHidden = useMemo(
|
|
||||||
() => (
|
|
||||||
<ViewHidden
|
|
||||||
items={hidden}
|
|
||||||
selected={controller.selected}
|
|
||||||
schema={controller.schema}
|
|
||||||
coloringScheme={coloring}
|
|
||||||
toggleSelection={controller.toggleSelect}
|
|
||||||
setFocus={handleSetFocus}
|
|
||||||
onEdit={onOpenEdit}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
[hidden, controller.selected, controller.schema, coloring, controller.toggleSelect, handleSetFocus, onOpenEdit]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<ReactFlowProvider>
|
||||||
<AnimatePresence>
|
<TGFlow onOpenEdit={onOpenEdit} />
|
||||||
{showParamsDialog ? (
|
</ReactFlowProvider>
|
||||||
<DlgGraphParams
|
|
||||||
hideWindow={() => setShowParamsDialog(false)}
|
|
||||||
initial={filterParams}
|
|
||||||
onConfirm={handleChangeParams}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</AnimatePresence>
|
|
||||||
|
|
||||||
<Overlay position='cc-tab-tools' className='flex flex-col items-center rounded-b-2xl cc-blur'>
|
|
||||||
<ToolbarTermGraph
|
|
||||||
is3D={is3D}
|
|
||||||
orbit={orbit}
|
|
||||||
noText={filterParams.noText}
|
|
||||||
foldDerived={filterParams.foldDerived}
|
|
||||||
showParamsDialog={() => setShowParamsDialog(true)}
|
|
||||||
onCreate={handleCreateCst}
|
|
||||||
onDelete={handleDeleteCst}
|
|
||||||
onFitView={() => setToggleResetView(prev => !prev)}
|
|
||||||
onSaveImage={handleSaveImage}
|
|
||||||
toggleOrbit={() => setOrbit(prev => !prev)}
|
|
||||||
toggleFoldDerived={handleFoldDerived}
|
|
||||||
toggleNoText={() =>
|
|
||||||
setFilterParams(prev => ({
|
|
||||||
...prev,
|
|
||||||
noText: !prev.noText
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{!focusCst ? (
|
|
||||||
<ToolbarGraphSelection
|
|
||||||
graph={controller.schema!.graph}
|
|
||||||
isCore={cstID => isBasicConcept(controller.schema?.cstByID.get(cstID)?.cst_type)}
|
|
||||||
isOwned={cstID => !controller.schema?.cstByID.get(cstID)?.is_inherited}
|
|
||||||
setSelected={controller.setSelected}
|
|
||||||
emptySelection={controller.selected.length === 0}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
{focusCst ? (
|
|
||||||
<ToolbarFocusedCst
|
|
||||||
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>
|
|
||||||
|
|
||||||
<AnimateFade tabIndex={-1} onKeyDown={handleKeyDown}>
|
|
||||||
<SelectedCounter
|
|
||||||
hideZero
|
|
||||||
totalCount={controller.schema?.stats?.count_all ?? 0}
|
|
||||||
selectedCount={controller.selected.length}
|
|
||||||
position='top-[4.3rem] sm:top-[2rem] left-0'
|
|
||||||
/>
|
|
||||||
|
|
||||||
{hoverCst && hoverCstDebounced && hoverCst === hoverCstDebounced ? (
|
|
||||||
<Overlay
|
|
||||||
layer='z-tooltip'
|
|
||||||
position={clsx('top-[3.5rem]', { 'left-[2.6rem]': hoverLeft, 'right-[2.6rem]': !hoverLeft })}
|
|
||||||
className={clsx(
|
|
||||||
'w-[25rem] max-h-[calc(100dvh-15rem)]',
|
|
||||||
'px-3',
|
|
||||||
'cc-scroll-y',
|
|
||||||
'border shadow-md',
|
|
||||||
'clr-input',
|
|
||||||
'text-sm'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<InfoConstituenta className='pt-1 pb-2' data={hoverCstDebounced} />
|
|
||||||
</Overlay>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<Overlay position='top-[8.15rem] sm:top-[5.9rem] left-0' className='flex gap-1'>
|
|
||||||
<div className='flex flex-col ml-2 w-[13.5rem]'>
|
|
||||||
{selectors}
|
|
||||||
{viewHidden}
|
|
||||||
</div>
|
|
||||||
</Overlay>
|
|
||||||
|
|
||||||
{graph}
|
|
||||||
</AnimateFade>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,57 +1,34 @@
|
||||||
import BadgeHelp from '@/components/info/BadgeHelp';
|
import BadgeHelp from '@/components/info/BadgeHelp';
|
||||||
import { GraphLayout } from '@/components/ui/GraphUI';
|
|
||||||
import Overlay from '@/components/ui/Overlay';
|
import Overlay from '@/components/ui/Overlay';
|
||||||
import SelectSingle from '@/components/ui/SelectSingle';
|
import SelectSingle from '@/components/ui/SelectSingle';
|
||||||
import { GraphColoring, GraphSizing, HelpTopic } from '@/models/miscellaneous';
|
import { GraphColoring, HelpTopic } from '@/models/miscellaneous';
|
||||||
import { IRSForm } from '@/models/rsform';
|
import { IRSForm } from '@/models/rsform';
|
||||||
import { mapLabelColoring, mapLabelLayout, mapLabelSizing } from '@/utils/labels';
|
import { mapLabelColoring } from '@/utils/labels';
|
||||||
import { SelectorGraphColoring, SelectorGraphLayout, SelectorGraphSizing } from '@/utils/selectors';
|
import { SelectorGraphColoring } from '@/utils/selectors';
|
||||||
|
|
||||||
import SchemasGuide from './SchemasGuide';
|
import SchemasGuide from './SchemasGuide';
|
||||||
|
|
||||||
interface GraphSelectorsProps {
|
interface GraphSelectorsProps {
|
||||||
schema?: IRSForm;
|
schema?: IRSForm;
|
||||||
coloring: GraphColoring;
|
coloring: GraphColoring;
|
||||||
layout: GraphLayout;
|
onChangeColoring: (newValue: GraphColoring) => void;
|
||||||
sizing: GraphSizing;
|
|
||||||
|
|
||||||
setLayout: (newValue: GraphLayout) => void;
|
|
||||||
setColoring: (newValue: GraphColoring) => void;
|
|
||||||
setSizing: (newValue: GraphSizing) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function GraphSelectors({ schema, coloring, setColoring, layout, setLayout, sizing, setSizing }: GraphSelectorsProps) {
|
function GraphSelectors({ schema, coloring, onChangeColoring }: GraphSelectorsProps) {
|
||||||
return (
|
return (
|
||||||
<div className='border rounded-b-none select-none clr-input rounded-t-md'>
|
<div className='border rounded-b-none select-none clr-input rounded-t-md'>
|
||||||
<SelectSingle
|
<Overlay position='right-[2.5rem] top-[0.25rem]'>
|
||||||
noBorder
|
|
||||||
placeholder='Способ расположения'
|
|
||||||
options={SelectorGraphLayout}
|
|
||||||
isSearchable={false}
|
|
||||||
value={layout ? { value: layout, label: mapLabelLayout.get(layout) } : null}
|
|
||||||
onChange={data => setLayout(data?.value ?? SelectorGraphLayout[0].value)}
|
|
||||||
/>
|
|
||||||
<Overlay position='right-[2.5rem] top-[0.5rem]'>
|
|
||||||
{coloring === 'status' ? <BadgeHelp topic={HelpTopic.UI_CST_STATUS} className='min-w-[25rem]' /> : null}
|
{coloring === 'status' ? <BadgeHelp topic={HelpTopic.UI_CST_STATUS} className='min-w-[25rem]' /> : null}
|
||||||
{coloring === 'type' ? <BadgeHelp topic={HelpTopic.UI_CST_CLASS} className='min-w-[25rem]' /> : null}
|
{coloring === 'type' ? <BadgeHelp topic={HelpTopic.UI_CST_CLASS} className='min-w-[25rem]' /> : null}
|
||||||
{coloring === 'schemas' && !!schema ? <SchemasGuide schema={schema} /> : null}
|
{coloring === 'schemas' && !!schema ? <SchemasGuide schema={schema} /> : null}
|
||||||
</Overlay>
|
</Overlay>
|
||||||
<SelectSingle
|
<SelectSingle
|
||||||
className='my-1'
|
|
||||||
noBorder
|
noBorder
|
||||||
placeholder='Цветовая схема'
|
placeholder='Цветовая схема'
|
||||||
options={SelectorGraphColoring}
|
options={SelectorGraphColoring}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
value={coloring ? { value: coloring, label: mapLabelColoring.get(coloring) } : null}
|
value={coloring ? { value: coloring, label: mapLabelColoring.get(coloring) } : null}
|
||||||
onChange={data => setColoring(data?.value ?? SelectorGraphColoring[0].value)}
|
onChange={data => onChangeColoring(data?.value ?? SelectorGraphColoring[0].value)}
|
||||||
/>
|
|
||||||
<SelectSingle
|
|
||||||
noBorder
|
|
||||||
placeholder='Размер узлов'
|
|
||||||
options={SelectorGraphSizing}
|
|
||||||
isSearchable={false}
|
|
||||||
value={layout ? { value: sizing, label: mapLabelSizing.get(sizing) } : null}
|
|
||||||
onChange={data => setSizing(data?.value ?? SelectorGraphSizing[0].value)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,480 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { AnimatePresence } from 'framer-motion';
|
||||||
|
import { toPng } from 'html-to-image';
|
||||||
|
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import {
|
||||||
|
Edge,
|
||||||
|
getNodesBounds,
|
||||||
|
getViewportForBounds,
|
||||||
|
MarkerType,
|
||||||
|
Node,
|
||||||
|
ReactFlow,
|
||||||
|
useEdgesState,
|
||||||
|
useNodesState,
|
||||||
|
useOnSelectionChange,
|
||||||
|
useReactFlow
|
||||||
|
} from 'reactflow';
|
||||||
|
import { useStoreApi } from 'reactflow';
|
||||||
|
import { useDebounce } from 'use-debounce';
|
||||||
|
|
||||||
|
import InfoConstituenta from '@/components/info/InfoConstituenta';
|
||||||
|
import SelectedCounter from '@/components/info/SelectedCounter';
|
||||||
|
import { CProps } from '@/components/props';
|
||||||
|
import ToolbarGraphSelection from '@/components/select/ToolbarGraphSelection';
|
||||||
|
import Overlay from '@/components/ui/Overlay';
|
||||||
|
import AnimateFade from '@/components/wrap/AnimateFade';
|
||||||
|
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||||
|
import DlgGraphParams from '@/dialogs/DlgGraphParams';
|
||||||
|
import useLocalStorage from '@/hooks/useLocalStorage';
|
||||||
|
import { GraphColoring, GraphFilterParams } from '@/models/miscellaneous';
|
||||||
|
import { ConstituentaID, CstType, IConstituenta } from '@/models/rsform';
|
||||||
|
import { isBasicConcept } from '@/models/rsformAPI';
|
||||||
|
import { colorBgGraphNode } from '@/styling/color';
|
||||||
|
import { PARAMETER, storage } from '@/utils/constants';
|
||||||
|
import { errors } from '@/utils/labels';
|
||||||
|
|
||||||
|
import { useRSEdit } from '../RSEditContext';
|
||||||
|
import { TGEdgeTypes } from './graph/TGEdgeTypes';
|
||||||
|
import { applyLayout } from './graph/TGLayout';
|
||||||
|
import { TGNodeData } from './graph/TGNode';
|
||||||
|
import { TGNodeTypes } from './graph/TGNodeTypes';
|
||||||
|
import GraphSelectors from './GraphSelectors';
|
||||||
|
import ToolbarFocusedCst from './ToolbarFocusedCst';
|
||||||
|
import ToolbarTermGraph from './ToolbarTermGraph';
|
||||||
|
import useGraphFilter from './useGraphFilter';
|
||||||
|
import ViewHidden from './ViewHidden';
|
||||||
|
|
||||||
|
const ZOOM_MAX = 3;
|
||||||
|
const ZOOM_MIN = 0.25;
|
||||||
|
|
||||||
|
interface TGFlowProps {
|
||||||
|
onOpenEdit: (cstID: ConstituentaID) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TGFlow({ onOpenEdit }: TGFlowProps) {
|
||||||
|
const { colors, mainHeight } = useConceptOptions();
|
||||||
|
const controller = useRSEdit();
|
||||||
|
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||||
|
const [edges, setEdges] = useEdgesState([]);
|
||||||
|
const flow = useReactFlow();
|
||||||
|
const store = useStoreApi();
|
||||||
|
|
||||||
|
const [filterParams, setFilterParams] = useLocalStorage<GraphFilterParams>(storage.rsgraphFilter, {
|
||||||
|
noHermits: true,
|
||||||
|
noTemplates: false,
|
||||||
|
noTransitive: true,
|
||||||
|
noText: false,
|
||||||
|
foldDerived: false,
|
||||||
|
|
||||||
|
focusShowInputs: true,
|
||||||
|
focusShowOutputs: true,
|
||||||
|
|
||||||
|
allowBase: true,
|
||||||
|
allowStruct: true,
|
||||||
|
allowTerm: true,
|
||||||
|
allowAxiom: true,
|
||||||
|
allowFunction: true,
|
||||||
|
allowPredicate: true,
|
||||||
|
allowConstant: true,
|
||||||
|
allowTheorem: true
|
||||||
|
});
|
||||||
|
const [showParamsDialog, setShowParamsDialog] = useState(false);
|
||||||
|
const [focusCst, setFocusCst] = useState<IConstituenta | undefined>(undefined);
|
||||||
|
const filteredGraph = useGraphFilter(controller.schema, filterParams, focusCst);
|
||||||
|
|
||||||
|
const [hidden, setHidden] = useState<ConstituentaID[]>([]);
|
||||||
|
|
||||||
|
const [coloring, setColoring] = useLocalStorage<GraphColoring>(storage.rsgraphColoring, 'type');
|
||||||
|
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
|
||||||
|
const [hoverID, setHoverID] = useState<ConstituentaID | undefined>(undefined);
|
||||||
|
const hoverCst = useMemo(() => {
|
||||||
|
return hoverID && controller.schema?.cstByID.get(hoverID);
|
||||||
|
}, [controller.schema?.cstByID, hoverID]);
|
||||||
|
const [hoverCstDebounced] = useDebounce(hoverCst, PARAMETER.graphPopupDelay);
|
||||||
|
const [hoverLeft, setHoverLeft] = useState(true);
|
||||||
|
|
||||||
|
const [toggleResetView, setToggleResetView] = useState(false);
|
||||||
|
|
||||||
|
const { addSelectedNodes } = store.getState();
|
||||||
|
|
||||||
|
const onSelectionChange = useCallback(
|
||||||
|
({ nodes }: { nodes: Node[] }) => {
|
||||||
|
const ids = nodes.map(node => Number(node.id));
|
||||||
|
if (ids.length === 0) {
|
||||||
|
controller.setSelected([]);
|
||||||
|
} else {
|
||||||
|
controller.setSelected(prev => [...prev.filter(nodeID => !filteredGraph.hasNode(nodeID)), ...ids]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[controller, filteredGraph]
|
||||||
|
);
|
||||||
|
|
||||||
|
useOnSelectionChange({
|
||||||
|
onChange: onSelectionChange
|
||||||
|
});
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!controller.schema) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newDismissed: ConstituentaID[] = [];
|
||||||
|
controller.schema.items.forEach(cst => {
|
||||||
|
if (!filteredGraph.nodes.has(cst.id)) {
|
||||||
|
newDismissed.push(cst.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setHidden(newDismissed);
|
||||||
|
setHoverID(undefined);
|
||||||
|
}, [controller.schema, filteredGraph]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!controller.schema) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newNodes: Node<TGNodeData>[] = [];
|
||||||
|
filteredGraph.nodes.forEach(node => {
|
||||||
|
const cst = controller.schema!.cstByID.get(node.id);
|
||||||
|
if (cst) {
|
||||||
|
newNodes.push({
|
||||||
|
id: String(node.id),
|
||||||
|
type: 'concept',
|
||||||
|
selected: controller.selected.includes(node.id),
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
data: {
|
||||||
|
fill: focusCst === cst ? colors.bgPurple : colorBgGraphNode(cst, coloring, colors),
|
||||||
|
label: cst.alias,
|
||||||
|
subLabel: !filterParams.noText ? cst.term_resolved : ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const newEdges: Edge[] = [];
|
||||||
|
let edgeID = 1;
|
||||||
|
filteredGraph.nodes.forEach(source => {
|
||||||
|
source.outputs.forEach(target => {
|
||||||
|
if (newNodes.find(node => node.id === String(target))) {
|
||||||
|
newEdges.push({
|
||||||
|
id: String(edgeID),
|
||||||
|
source: String(source.id),
|
||||||
|
target: String(target),
|
||||||
|
type: 'termEdge',
|
||||||
|
focusable: false,
|
||||||
|
markerEnd: {
|
||||||
|
type: MarkerType.ArrowClosed,
|
||||||
|
width: 20,
|
||||||
|
height: 20
|
||||||
|
}
|
||||||
|
});
|
||||||
|
edgeID += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
applyLayout(newNodes, newEdges, !filterParams.noText);
|
||||||
|
|
||||||
|
setNodes(newNodes);
|
||||||
|
setEdges(newEdges);
|
||||||
|
// NOTE: Do not rerender on controller.selected change because it is only needed during first load
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [filteredGraph, setNodes, setEdges, controller.schema, filterParams.noText, focusCst, coloring, colors, flow]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
flow.fitView({ duration: PARAMETER.zoomDuration });
|
||||||
|
}, PARAMETER.minimalTimeout);
|
||||||
|
}, [toggleResetView, flow, focusCst, filterParams]);
|
||||||
|
|
||||||
|
function handleSetSelected(newSelection: number[]) {
|
||||||
|
controller.setSelected(newSelection);
|
||||||
|
addSelectedNodes(newSelection.map(id => String(id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCreateCst() {
|
||||||
|
if (!controller.schema) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const definition = controller.selected.map(id => controller.schema!.cstByID.get(id)!.alias).join(' ');
|
||||||
|
controller.createCst(controller.selected.length === 0 ? CstType.BASE : CstType.TERM, false, definition);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeleteCst() {
|
||||||
|
if (!controller.schema || !controller.canDeleteSelected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
controller.promptDeleteCst();
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChangeParams = useCallback(
|
||||||
|
(params: GraphFilterParams) => {
|
||||||
|
setFilterParams(params);
|
||||||
|
},
|
||||||
|
[setFilterParams]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSaveImage = useCallback(() => {
|
||||||
|
if (!controller.schema) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const canvas: HTMLElement | null = document.querySelector('.react-flow__viewport');
|
||||||
|
if (canvas === null) {
|
||||||
|
toast.error(errors.imageFailed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageWidth = PARAMETER.ossImageWidth;
|
||||||
|
const imageHeight = PARAMETER.ossImageHeight;
|
||||||
|
const nodesBounds = getNodesBounds(nodes);
|
||||||
|
const viewport = getViewportForBounds(nodesBounds, imageWidth, imageHeight, ZOOM_MIN, ZOOM_MAX);
|
||||||
|
toPng(canvas, {
|
||||||
|
backgroundColor: colors.bgDefault,
|
||||||
|
width: imageWidth,
|
||||||
|
height: imageHeight,
|
||||||
|
style: {
|
||||||
|
width: String(imageWidth),
|
||||||
|
height: String(imageHeight),
|
||||||
|
transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom * 2})`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(dataURL => {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.setAttribute('download', `${controller.schema?.alias ?? 'graph'}.png`);
|
||||||
|
a.setAttribute('href', dataURL);
|
||||||
|
a.click();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error);
|
||||||
|
toast.error(errors.imageFailed);
|
||||||
|
});
|
||||||
|
}, [colors, nodes, controller.schema]);
|
||||||
|
|
||||||
|
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||||
|
if (controller.isProcessing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
setFocusCst(undefined);
|
||||||
|
handleSetSelected([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!controller.isContentEditable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.key === 'Delete') {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
handleDeleteCst();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFoldDerived = useCallback(() => {
|
||||||
|
setFilterParams(prev => ({
|
||||||
|
...prev,
|
||||||
|
foldDerived: !prev.foldDerived
|
||||||
|
}));
|
||||||
|
setTimeout(() => {
|
||||||
|
setToggleResetView(prev => !prev);
|
||||||
|
}, 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 handleNodeClick = useCallback(
|
||||||
|
(event: CProps.EventMouse, cstID: ConstituentaID) => {
|
||||||
|
if (event.altKey) {
|
||||||
|
handleSetFocus(cstID);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[handleSetFocus]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleNodeDoubleClick = useCallback(
|
||||||
|
(event: CProps.EventMouse, cstID: ConstituentaID) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
onOpenEdit(cstID);
|
||||||
|
},
|
||||||
|
[onOpenEdit]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleNodeEnter = useCallback(
|
||||||
|
(event: CProps.EventMouse, cstID: ConstituentaID) => {
|
||||||
|
setHoverID(cstID);
|
||||||
|
setHoverLeft(
|
||||||
|
event.clientX / window.innerWidth >= PARAMETER.graphHoverXLimit ||
|
||||||
|
event.clientY / window.innerHeight >= PARAMETER.graphHoverYLimit
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[setHoverID, setHoverLeft]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleNodeLeave = useCallback(() => {
|
||||||
|
setHoverID(undefined);
|
||||||
|
}, [setHoverID]);
|
||||||
|
|
||||||
|
const selectors = useMemo(
|
||||||
|
() => <GraphSelectors schema={controller.schema} coloring={coloring} onChangeColoring={setColoring} />,
|
||||||
|
[coloring, controller.schema, setColoring]
|
||||||
|
);
|
||||||
|
const viewHidden = useMemo(
|
||||||
|
() => (
|
||||||
|
<ViewHidden
|
||||||
|
items={hidden}
|
||||||
|
selected={controller.selected}
|
||||||
|
schema={controller.schema}
|
||||||
|
coloringScheme={coloring}
|
||||||
|
toggleSelection={controller.toggleSelect}
|
||||||
|
setFocus={handleSetFocus}
|
||||||
|
onEdit={onOpenEdit}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[hidden, controller.selected, controller.schema, coloring, controller.toggleSelect, handleSetFocus, onOpenEdit]
|
||||||
|
);
|
||||||
|
|
||||||
|
const graph = useMemo(
|
||||||
|
() => (
|
||||||
|
<div className='relative outline-none w-[100dvw]' style={{ height: mainHeight }}>
|
||||||
|
<ReactFlow
|
||||||
|
nodes={nodes}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
edges={edges}
|
||||||
|
fitView
|
||||||
|
edgesFocusable={false}
|
||||||
|
nodesFocusable={false}
|
||||||
|
nodesConnectable={false}
|
||||||
|
nodeTypes={TGNodeTypes}
|
||||||
|
edgeTypes={TGEdgeTypes}
|
||||||
|
maxZoom={ZOOM_MAX}
|
||||||
|
minZoom={ZOOM_MIN}
|
||||||
|
onNodeDragStart={() => setIsDragging(true)}
|
||||||
|
onNodeDragStop={() => setIsDragging(false)}
|
||||||
|
onNodeMouseEnter={(event, node) => handleNodeEnter(event, Number(node.id))}
|
||||||
|
onNodeMouseLeave={handleNodeLeave}
|
||||||
|
onNodeClick={(event, node) => handleNodeClick(event, Number(node.id))}
|
||||||
|
onNodeDoubleClick={(event, node) => handleNodeDoubleClick(event, Number(node.id))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
[nodes, edges, mainHeight, handleNodeClick, handleNodeDoubleClick, handleNodeLeave, handleNodeEnter, onNodesChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AnimatePresence>
|
||||||
|
{showParamsDialog ? (
|
||||||
|
<DlgGraphParams
|
||||||
|
hideWindow={() => setShowParamsDialog(false)}
|
||||||
|
initial={filterParams}
|
||||||
|
onConfirm={handleChangeParams}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
<AnimateFade tabIndex={-1} onKeyDown={handleKeyDown}>
|
||||||
|
<Overlay position='cc-tab-tools' className='flex flex-col items-center rounded-b-2xl cc-blur'>
|
||||||
|
<ToolbarTermGraph
|
||||||
|
noText={filterParams.noText}
|
||||||
|
foldDerived={filterParams.foldDerived}
|
||||||
|
showParamsDialog={() => setShowParamsDialog(true)}
|
||||||
|
onCreate={handleCreateCst}
|
||||||
|
onDelete={handleDeleteCst}
|
||||||
|
onFitView={() => setToggleResetView(prev => !prev)}
|
||||||
|
onSaveImage={handleSaveImage}
|
||||||
|
toggleFoldDerived={handleFoldDerived}
|
||||||
|
toggleNoText={() =>
|
||||||
|
setFilterParams(prev => ({
|
||||||
|
...prev,
|
||||||
|
noText: !prev.noText
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{!focusCst ? (
|
||||||
|
<ToolbarGraphSelection
|
||||||
|
graph={controller.schema!.graph}
|
||||||
|
isCore={cstID => isBasicConcept(controller.schema?.cstByID.get(cstID)?.cst_type)}
|
||||||
|
isOwned={
|
||||||
|
controller.schema && controller.schema.inheritance.length > 0
|
||||||
|
? cstID => !controller.schema!.cstByID.get(cstID)?.is_inherited
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
selected={controller.selected}
|
||||||
|
setSelected={handleSetSelected}
|
||||||
|
emptySelection={controller.selected.length === 0}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{focusCst ? (
|
||||||
|
<ToolbarFocusedCst
|
||||||
|
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>
|
||||||
|
|
||||||
|
<SelectedCounter
|
||||||
|
hideZero
|
||||||
|
totalCount={controller.schema?.stats?.count_all ?? 0}
|
||||||
|
selectedCount={controller.selected.length}
|
||||||
|
position='top-[4.3rem] sm:top-[2rem] left-0'
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!isDragging && hoverCst && hoverCstDebounced && hoverCst === hoverCstDebounced ? (
|
||||||
|
<Overlay
|
||||||
|
layer='z-tooltip'
|
||||||
|
position={clsx('top-[3.5rem]', { 'left-[2.6rem]': hoverLeft, 'right-[2.6rem]': !hoverLeft })}
|
||||||
|
className={clsx(
|
||||||
|
'w-[25rem] max-h-[calc(100dvh-15rem)]',
|
||||||
|
'px-3',
|
||||||
|
'cc-scroll-y',
|
||||||
|
'border shadow-md',
|
||||||
|
'clr-input',
|
||||||
|
'text-sm'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<InfoConstituenta className='pt-1 pb-2' data={hoverCstDebounced} />
|
||||||
|
</Overlay>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Overlay position='top-[8.15rem] sm:top-[5.9rem] left-0' className='flex gap-1'>
|
||||||
|
<div className='flex flex-col ml-2 w-[13.5rem]'>
|
||||||
|
{selectors}
|
||||||
|
{viewHidden}
|
||||||
|
</div>
|
||||||
|
</Overlay>
|
||||||
|
{graph}
|
||||||
|
</AnimateFade>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TGFlow;
|
|
@ -1,137 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { RefObject, useCallback, useLayoutEffect } from 'react';
|
|
||||||
|
|
||||||
import GraphUI, {
|
|
||||||
CollapseProps,
|
|
||||||
GraphCanvasRef,
|
|
||||||
GraphEdge,
|
|
||||||
GraphLayout,
|
|
||||||
GraphMouseEvent,
|
|
||||||
GraphNode,
|
|
||||||
GraphPointerEvent,
|
|
||||||
useSelection
|
|
||||||
} from '@/components/ui/GraphUI';
|
|
||||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
|
||||||
import { ConstituentaID } from '@/models/rsform';
|
|
||||||
import { graphDarkT, graphLightT } from '@/styling/color';
|
|
||||||
import { PARAMETER, resources } from '@/utils/constants';
|
|
||||||
|
|
||||||
interface TermGraphProps {
|
|
||||||
graphRef: RefObject<GraphCanvasRef>;
|
|
||||||
nodes: GraphNode[];
|
|
||||||
edges: GraphEdge[];
|
|
||||||
selectedIDs: ConstituentaID[];
|
|
||||||
|
|
||||||
layout: GraphLayout;
|
|
||||||
is3D: boolean;
|
|
||||||
orbit: boolean;
|
|
||||||
|
|
||||||
setHoverID: (newID: ConstituentaID | undefined) => void;
|
|
||||||
setHoverLeft: (value: boolean) => void;
|
|
||||||
onEdit: (cstID: ConstituentaID) => void;
|
|
||||||
onSelectCentral: (selectedID: ConstituentaID) => void;
|
|
||||||
onSelect: (newID: ConstituentaID) => void;
|
|
||||||
onDeselect: (newID: ConstituentaID) => void;
|
|
||||||
|
|
||||||
toggleResetView: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function TermGraph({
|
|
||||||
graphRef,
|
|
||||||
nodes,
|
|
||||||
edges,
|
|
||||||
selectedIDs,
|
|
||||||
layout,
|
|
||||||
is3D,
|
|
||||||
orbit,
|
|
||||||
toggleResetView,
|
|
||||||
setHoverID,
|
|
||||||
setHoverLeft,
|
|
||||||
onEdit,
|
|
||||||
onSelectCentral,
|
|
||||||
onSelect,
|
|
||||||
onDeselect
|
|
||||||
}: TermGraphProps) {
|
|
||||||
const { mainHeight, darkMode } = useConceptOptions();
|
|
||||||
|
|
||||||
const { selections, setSelections } = useSelection({
|
|
||||||
ref: graphRef,
|
|
||||||
nodes: nodes,
|
|
||||||
edges: edges,
|
|
||||||
type: 'multi',
|
|
||||||
focusOnSelect: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleHoverIn = useCallback(
|
|
||||||
(node: GraphNode, event: GraphPointerEvent) => {
|
|
||||||
setHoverID(Number(node.id));
|
|
||||||
setHoverLeft(
|
|
||||||
event.clientX / window.innerWidth >= PARAMETER.graphHoverXLimit ||
|
|
||||||
event.clientY / window.innerHeight >= PARAMETER.graphHoverYLimit
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[setHoverID, setHoverLeft]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleHoverOut = useCallback(() => {
|
|
||||||
setHoverID(undefined);
|
|
||||||
}, [setHoverID]);
|
|
||||||
|
|
||||||
const handleNodeClick = useCallback(
|
|
||||||
(node: GraphNode, _?: CollapseProps, event?: GraphMouseEvent) => {
|
|
||||||
if (event?.ctrlKey || event?.metaKey) {
|
|
||||||
onSelectCentral(Number(node.id));
|
|
||||||
} else if (selections.includes(node.id)) {
|
|
||||||
onDeselect(Number(node.id));
|
|
||||||
} else {
|
|
||||||
onSelect(Number(node.id));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[onSelect, selections, onDeselect, onSelectCentral]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleNodeDoubleClick = useCallback(
|
|
||||||
(node: GraphNode) => {
|
|
||||||
onEdit(Number(node.id));
|
|
||||||
},
|
|
||||||
[onEdit]
|
|
||||||
);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
graphRef.current?.fitNodesInView([], { animated: true });
|
|
||||||
}, [toggleResetView, graphRef]);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
const newSelections = nodes.filter(node => selectedIDs.includes(Number(node.id))).map(node => node.id);
|
|
||||||
setSelections(newSelections);
|
|
||||||
}, [selectedIDs, setSelections, nodes]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='relative outline-none w-[100dvw]' style={{ height: mainHeight }}>
|
|
||||||
<GraphUI
|
|
||||||
nodes={nodes}
|
|
||||||
edges={edges}
|
|
||||||
ref={graphRef}
|
|
||||||
animated={false}
|
|
||||||
draggable
|
|
||||||
layoutType={layout}
|
|
||||||
selections={selections}
|
|
||||||
onNodeDoubleClick={handleNodeDoubleClick}
|
|
||||||
onNodeClick={handleNodeClick}
|
|
||||||
onNodePointerOver={handleHoverIn}
|
|
||||||
onNodePointerOut={handleHoverOut}
|
|
||||||
minNodeSize={4}
|
|
||||||
maxNodeSize={8}
|
|
||||||
cameraMode={orbit ? 'orbit' : is3D ? 'rotate' : 'pan'}
|
|
||||||
layoutOverrides={
|
|
||||||
layout.includes('tree') ? { nodeLevelRatio: nodes.length < PARAMETER.smallTreeNodes ? 3 : 1 } : undefined
|
|
||||||
}
|
|
||||||
labelFontUrl={resources.graph_font}
|
|
||||||
theme={darkMode ? graphDarkT : graphLightT}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TermGraph;
|
|
|
@ -8,7 +8,6 @@ import {
|
||||||
IconFitImage,
|
IconFitImage,
|
||||||
IconImage,
|
IconImage,
|
||||||
IconNewItem,
|
IconNewItem,
|
||||||
IconRotate3D,
|
|
||||||
IconText,
|
IconText,
|
||||||
IconTextOff,
|
IconTextOff,
|
||||||
IconTypeGraph
|
IconTypeGraph
|
||||||
|
@ -22,9 +21,6 @@ import { PARAMETER } from '@/utils/constants';
|
||||||
import { useRSEdit } from '../RSEditContext';
|
import { useRSEdit } from '../RSEditContext';
|
||||||
|
|
||||||
interface ToolbarTermGraphProps {
|
interface ToolbarTermGraphProps {
|
||||||
is3D: boolean;
|
|
||||||
|
|
||||||
orbit: boolean;
|
|
||||||
noText: boolean;
|
noText: boolean;
|
||||||
foldDerived: boolean;
|
foldDerived: boolean;
|
||||||
|
|
||||||
|
@ -36,17 +32,13 @@ interface ToolbarTermGraphProps {
|
||||||
|
|
||||||
toggleFoldDerived: () => void;
|
toggleFoldDerived: () => void;
|
||||||
toggleNoText: () => void;
|
toggleNoText: () => void;
|
||||||
toggleOrbit: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ToolbarTermGraph({
|
function ToolbarTermGraph({
|
||||||
is3D,
|
|
||||||
noText,
|
noText,
|
||||||
foldDerived,
|
foldDerived,
|
||||||
toggleNoText,
|
toggleNoText,
|
||||||
toggleFoldDerived,
|
toggleFoldDerived,
|
||||||
orbit,
|
|
||||||
toggleOrbit,
|
|
||||||
showParamsDialog,
|
showParamsDialog,
|
||||||
onCreate,
|
onCreate,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
@ -95,12 +87,6 @@ function ToolbarTermGraph({
|
||||||
}
|
}
|
||||||
onClick={toggleFoldDerived}
|
onClick={toggleFoldDerived}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
|
||||||
icon={<IconRotate3D size='1.25rem' className={orbit ? 'icon-green' : 'icon-primary'} />}
|
|
||||||
title='Анимация вращения'
|
|
||||||
disabled={!is3D}
|
|
||||||
onClick={toggleOrbit}
|
|
||||||
/>
|
|
||||||
{controller.isContentEditable ? (
|
{controller.isContentEditable ? (
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Новая конституента'
|
title='Новая конституента'
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { EdgeProps, getStraightPath } from 'reactflow';
|
||||||
|
|
||||||
|
import { PARAMETER } from '@/utils/constants';
|
||||||
|
|
||||||
|
function TGEdge({ id, markerEnd, style, ...props }: EdgeProps) {
|
||||||
|
const sourceY = props.sourceY - PARAMETER.graphNodeRadius;
|
||||||
|
const targetY = props.targetY + PARAMETER.graphNodeRadius;
|
||||||
|
|
||||||
|
const scale =
|
||||||
|
(PARAMETER.graphNodePadding + PARAMETER.graphNodeRadius) /
|
||||||
|
Math.sqrt(Math.pow(props.sourceX - props.targetX, 2) + Math.pow(Math.abs(sourceY - targetY), 2));
|
||||||
|
|
||||||
|
const [path] = getStraightPath({
|
||||||
|
sourceX: props.sourceX - (props.sourceX - props.targetX) * scale,
|
||||||
|
sourceY: sourceY - (sourceY - targetY) * scale,
|
||||||
|
targetX: props.targetX + (props.sourceX - props.targetX) * scale,
|
||||||
|
targetY: targetY + (sourceY - targetY) * scale
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<path id={id} className='react-flow__edge-path' d={path} markerEnd={markerEnd} style={style} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TGEdge;
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { EdgeTypes } from 'reactflow';
|
||||||
|
|
||||||
|
import TGEdge from './TGEdge';
|
||||||
|
|
||||||
|
export const TGEdgeTypes: EdgeTypes = {
|
||||||
|
termEdge: TGEdge
|
||||||
|
};
|
|
@ -0,0 +1,32 @@
|
||||||
|
import dagre from '@dagrejs/dagre';
|
||||||
|
import { Edge, Node } from 'reactflow';
|
||||||
|
|
||||||
|
import { PARAMETER } from '@/utils/constants';
|
||||||
|
|
||||||
|
import { TGNodeData } from './TGNode';
|
||||||
|
|
||||||
|
export function applyLayout(nodes: Node<TGNodeData>[], edges: Edge[], subLabels?: boolean) {
|
||||||
|
const dagreGraph = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
||||||
|
dagreGraph.setGraph({
|
||||||
|
rankdir: 'TB',
|
||||||
|
ranksep: subLabels ? 60 : 40,
|
||||||
|
nodesep: subLabels ? 100 : 20,
|
||||||
|
ranker: 'network-simplex',
|
||||||
|
align: undefined
|
||||||
|
});
|
||||||
|
nodes.forEach(node => {
|
||||||
|
dagreGraph.setNode(node.id, { width: 2 * PARAMETER.graphNodeRadius, height: 2 * PARAMETER.graphNodeRadius });
|
||||||
|
});
|
||||||
|
|
||||||
|
edges.forEach(edge => {
|
||||||
|
dagreGraph.setEdge(edge.source, edge.target);
|
||||||
|
});
|
||||||
|
|
||||||
|
dagre.layout(dagreGraph);
|
||||||
|
|
||||||
|
nodes.forEach(node => {
|
||||||
|
const nodeWithPosition = dagreGraph.node(node.id);
|
||||||
|
node.position.x = nodeWithPosition.x - PARAMETER.graphNodeRadius;
|
||||||
|
node.position.y = nodeWithPosition.y - PARAMETER.graphNodeRadius;
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { Handle, Position } from 'reactflow';
|
||||||
|
|
||||||
|
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||||
|
import { truncateToLastWord } from '@/utils/utils';
|
||||||
|
|
||||||
|
const MAX_LABEL_LENGTH = 65;
|
||||||
|
|
||||||
|
export interface TGNodeData {
|
||||||
|
fill: string;
|
||||||
|
label: string;
|
||||||
|
subLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents graph AST node internal data.
|
||||||
|
*/
|
||||||
|
interface TGNodeInternal {
|
||||||
|
id: string;
|
||||||
|
data: TGNodeData;
|
||||||
|
selected: boolean;
|
||||||
|
dragging: boolean;
|
||||||
|
xPos: number;
|
||||||
|
yPos: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TGNode(node: TGNodeInternal) {
|
||||||
|
const { colors } = useConceptOptions();
|
||||||
|
const subLabel = useMemo(() => truncateToLastWord(node.data.subLabel, MAX_LABEL_LENGTH), [node.data.subLabel]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Handle type='target' position={Position.Top} style={{ opacity: 0 }} />
|
||||||
|
<div
|
||||||
|
className='w-full h-full cursor-default flex items-center justify-center rounded-full'
|
||||||
|
style={{
|
||||||
|
backgroundColor: !node.selected ? node.data.fill : colors.bgActiveSelection,
|
||||||
|
outlineOffset: '4px',
|
||||||
|
outlineStyle: 'solid',
|
||||||
|
outlineColor: node.selected ? colors.bgActiveSelection : 'transparent'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className='absolute top-[9px] left-0 text-center w-full'>{node.data.label}</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
WebkitTextStrokeWidth: 2,
|
||||||
|
WebkitTextStrokeColor: colors.bgDefault
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{node.data.label}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Handle type='source' position={Position.Bottom} style={{ opacity: 0 }} />
|
||||||
|
{subLabel ? (
|
||||||
|
<div
|
||||||
|
className='mt-1 w-[150px] px-1 text-center translate-x-[calc(-50%+20px)]'
|
||||||
|
style={{
|
||||||
|
fontSize: subLabel.length > 15 ? 10 : 12
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
WebkitTextStrokeWidth: 3,
|
||||||
|
WebkitTextStrokeColor: colors.bgDefault
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{subLabel}
|
||||||
|
</div>
|
||||||
|
<div className='absolute top-0 px-1 left-0 text-center w-full'>{subLabel}</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TGNode;
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { NodeTypes } from 'reactflow';
|
||||||
|
|
||||||
|
import TGNode from './TGNode';
|
||||||
|
|
||||||
|
export const TGNodeTypes: NodeTypes = {
|
||||||
|
concept: TGNode
|
||||||
|
};
|
|
@ -20,6 +20,7 @@ export interface IColorTheme {
|
||||||
bgDisabled: string;
|
bgDisabled: string;
|
||||||
bgPrimary: string;
|
bgPrimary: string;
|
||||||
bgSelected: string;
|
bgSelected: string;
|
||||||
|
bgActiveSelection: string;
|
||||||
bgHover: string;
|
bgHover: string;
|
||||||
bgWarning: string;
|
bgWarning: string;
|
||||||
|
|
||||||
|
@ -62,6 +63,7 @@ export const lightT: IColorTheme = {
|
||||||
bgDisabled: 'var(--cl-bg-60)',
|
bgDisabled: 'var(--cl-bg-60)',
|
||||||
bgPrimary: 'var(--cl-prim-bg-100)',
|
bgPrimary: 'var(--cl-prim-bg-100)',
|
||||||
bgSelected: 'var(--cl-prim-bg-80)',
|
bgSelected: 'var(--cl-prim-bg-80)',
|
||||||
|
bgActiveSelection: 'var(--cl-teal-bg-100)',
|
||||||
bgHover: 'var(--cl-prim-bg-60)',
|
bgHover: 'var(--cl-prim-bg-60)',
|
||||||
bgWarning: 'var(--cl-red-bg-100)',
|
bgWarning: 'var(--cl-red-bg-100)',
|
||||||
|
|
||||||
|
@ -104,6 +106,7 @@ export const darkT: IColorTheme = {
|
||||||
bgDisabled: 'var(--cd-bg-60)',
|
bgDisabled: 'var(--cd-bg-60)',
|
||||||
bgPrimary: 'var(--cd-prim-bg-100)',
|
bgPrimary: 'var(--cd-prim-bg-100)',
|
||||||
bgSelected: 'var(--cd-prim-bg-80)',
|
bgSelected: 'var(--cd-prim-bg-80)',
|
||||||
|
bgActiveSelection: 'var(--cd-teal-bg-100)',
|
||||||
bgHover: 'var(--cd-prim-bg-60)',
|
bgHover: 'var(--cd-prim-bg-60)',
|
||||||
bgWarning: 'var(--cd-red-bg-100)',
|
bgWarning: 'var(--cd-red-bg-100)',
|
||||||
|
|
||||||
|
@ -184,96 +187,6 @@ export const selectDarkT = {
|
||||||
neutral90: darkT.fgWarning
|
neutral90: darkT.fgWarning
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents Graph component Light theme.
|
|
||||||
*/
|
|
||||||
export const graphLightT = {
|
|
||||||
canvas: {
|
|
||||||
background: '#f9fafb'
|
|
||||||
},
|
|
||||||
node: {
|
|
||||||
fill: '#7ca0ab',
|
|
||||||
activeFill: '#1DE9AC',
|
|
||||||
opacity: 1,
|
|
||||||
selectedOpacity: 1,
|
|
||||||
inactiveOpacity: 1,
|
|
||||||
label: {
|
|
||||||
color: '#2A6475',
|
|
||||||
stroke: '#fff',
|
|
||||||
activeColor: '#1DE9AC'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
lasso: {
|
|
||||||
border: '1px solid #55aaff',
|
|
||||||
background: 'rgba(75, 160, 255, 0.1)'
|
|
||||||
},
|
|
||||||
ring: {
|
|
||||||
fill: '#D8E6EA',
|
|
||||||
activeFill: '#1DE9AC'
|
|
||||||
},
|
|
||||||
edge: {
|
|
||||||
fill: '#D8E6EA',
|
|
||||||
activeFill: '#1DE9AC',
|
|
||||||
opacity: 1,
|
|
||||||
selectedOpacity: 1,
|
|
||||||
inactiveOpacity: 1,
|
|
||||||
label: {
|
|
||||||
stroke: '#fff',
|
|
||||||
color: '#2A6475',
|
|
||||||
activeColor: '#1DE9AC'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
arrow: {
|
|
||||||
fill: '#D8E6EA',
|
|
||||||
activeFill: '#1DE9AC'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents Graph component Dark theme.
|
|
||||||
*/
|
|
||||||
export const graphDarkT = {
|
|
||||||
canvas: {
|
|
||||||
background: '#171717' // var(--cd-bg-100)
|
|
||||||
},
|
|
||||||
node: {
|
|
||||||
fill: '#7a8c9e',
|
|
||||||
activeFill: '#1DE9AC',
|
|
||||||
opacity: 1,
|
|
||||||
selectedOpacity: 1,
|
|
||||||
inactiveOpacity: 1,
|
|
||||||
label: {
|
|
||||||
stroke: '#1E2026',
|
|
||||||
color: '#ACBAC7',
|
|
||||||
activeColor: '#1DE9AC'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
lasso: {
|
|
||||||
border: '1px solid #55aaff',
|
|
||||||
background: 'rgba(75, 160, 255, 0.1)'
|
|
||||||
},
|
|
||||||
ring: {
|
|
||||||
fill: '#54616D',
|
|
||||||
activeFill: '#1DE9AC'
|
|
||||||
},
|
|
||||||
edge: {
|
|
||||||
fill: '#474B56',
|
|
||||||
activeFill: '#1DE9AC',
|
|
||||||
opacity: 1,
|
|
||||||
selectedOpacity: 1,
|
|
||||||
inactiveOpacity: 1,
|
|
||||||
label: {
|
|
||||||
stroke: '#1E2026',
|
|
||||||
color: '#ACBAC7',
|
|
||||||
activeColor: '#1DE9AC'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
arrow: {
|
|
||||||
fill: '#474B56',
|
|
||||||
activeFill: '#1DE9AC'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents Brackets highlights Light theme.
|
* Represents Brackets highlights Light theme.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -38,6 +38,8 @@
|
||||||
--cl-red-fg-100: hsl(000, 072%, 051%);
|
--cl-red-fg-100: hsl(000, 072%, 051%);
|
||||||
--cl-green-fg-100: hsl(120, 080%, 37%);
|
--cl-green-fg-100: hsl(120, 080%, 37%);
|
||||||
|
|
||||||
|
--cl-teal-bg-100: hsl(162, 082%, 051%);
|
||||||
|
|
||||||
/* Dark Theme */
|
/* Dark Theme */
|
||||||
--cd-bg-120: hsl(000, 000%, 005%);
|
--cd-bg-120: hsl(000, 000%, 005%);
|
||||||
--cd-bg-100: hsl(000, 000%, 009%);
|
--cd-bg-100: hsl(000, 000%, 009%);
|
||||||
|
@ -59,4 +61,6 @@
|
||||||
--cd-red-bg-100: hsl(000, 100%, 015%);
|
--cd-red-bg-100: hsl(000, 100%, 015%);
|
||||||
--cd-red-fg-100: hsl(000, 080%, 055%);
|
--cd-red-fg-100: hsl(000, 080%, 055%);
|
||||||
--cd-green-fg-100: hsl(120, 080%, 042%);
|
--cd-green-fg-100: hsl(120, 080%, 042%);
|
||||||
|
|
||||||
|
--cd-teal-bg-100: hsl(162, 082%, 041%);
|
||||||
}
|
}
|
||||||
|
|
|
@ -127,10 +127,17 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-flow__node-step,
|
.react-flow__node-step,
|
||||||
.react-flow__node-token {
|
.react-flow__node-token,
|
||||||
|
.react-flow__node-concept {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.react-flow__node-concept {
|
||||||
|
&.selected {
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -10,8 +10,8 @@ export const PARAMETER = {
|
||||||
smallTreeNodes: 50, // amount of nodes threshold for size increase for large graphs
|
smallTreeNodes: 50, // amount of nodes threshold for size increase for large graphs
|
||||||
refreshTimeout: 100, // milliseconds delay for post-refresh actions
|
refreshTimeout: 100, // milliseconds delay for post-refresh actions
|
||||||
minimalTimeout: 10, // milliseconds delay for fast updates
|
minimalTimeout: 10, // milliseconds delay for fast updates
|
||||||
|
|
||||||
zoomDuration: 500, // milliseconds animation duration
|
zoomDuration: 500, // milliseconds animation duration
|
||||||
|
|
||||||
ossImageWidth: 1280, // pixels - size of OSS image
|
ossImageWidth: 1280, // pixels - size of OSS image
|
||||||
ossImageHeight: 960, // pixels - size of OSS image
|
ossImageHeight: 960, // pixels - size of OSS image
|
||||||
ossContextMenuWidth: 200, // pixels - width of OSS context menu
|
ossContextMenuWidth: 200, // pixels - width of OSS context menu
|
||||||
|
@ -21,16 +21,16 @@ export const PARAMETER = {
|
||||||
ossDistanceX: 180, // pixels - insert x-distance between node centers
|
ossDistanceX: 180, // pixels - insert x-distance between node centers
|
||||||
ossDistanceY: 100, // pixels - insert y-distance between node centers
|
ossDistanceY: 100, // pixels - insert y-distance between node centers
|
||||||
|
|
||||||
|
graphNodeRadius: 20, // pixels - radius of graph node
|
||||||
|
graphNodePadding: 5, // pixels - padding of graph node
|
||||||
graphHoverXLimit: 0.4, // ratio to clientWidth used to determine which side of screen popup should be
|
graphHoverXLimit: 0.4, // ratio to clientWidth used to determine which side of screen popup should be
|
||||||
graphHoverYLimit: 0.6, // ratio to clientHeight used to determine which side of screen popup should be
|
graphHoverYLimit: 0.6, // ratio to clientHeight used to determine which side of screen popup should be
|
||||||
graphPopupDelay: 500, // milliseconds delay for graph popup selections
|
graphPopupDelay: 500, // milliseconds delay for graph popup selections
|
||||||
graphRefreshDelay: 10, // milliseconds delay for graph viewpoint reset
|
graphRefreshDelay: 10, // milliseconds delay for graph viewpoint reset
|
||||||
|
|
||||||
typificationTruncate: 42, // characters - threshold for long typification - truncate
|
typificationTruncate: 42, // characters - threshold for long typification - truncate
|
||||||
|
|
||||||
ossLongLabel: 14, // characters - threshold for long labels - small font
|
ossLongLabel: 14, // characters - threshold for long labels - small font
|
||||||
ossTruncateLabel: 32, // characters - threshold for long labels - truncate
|
ossTruncateLabel: 32, // characters - threshold for long labels - truncate
|
||||||
|
|
||||||
statSmallThreshold: 3, // characters - threshold for small labels - small font
|
statSmallThreshold: 3, // characters - threshold for small labels - small font
|
||||||
|
|
||||||
logicLabel: 'LOGIC',
|
logicLabel: 'LOGIC',
|
||||||
|
@ -121,9 +121,7 @@ export const storage = {
|
||||||
libraryPagination: 'library.pagination',
|
libraryPagination: 'library.pagination',
|
||||||
|
|
||||||
rsgraphFilter: 'rsgraph.filter2',
|
rsgraphFilter: 'rsgraph.filter2',
|
||||||
rsgraphLayout: 'rsgraph.layout',
|
|
||||||
rsgraphColoring: 'rsgraph.coloring',
|
rsgraphColoring: 'rsgraph.coloring',
|
||||||
rsgraphSizing: 'rsgraph.sizing',
|
|
||||||
rsgraphFoldHidden: 'rsgraph.fold_hidden',
|
rsgraphFoldHidden: 'rsgraph.fold_hidden',
|
||||||
|
|
||||||
ossShowGrid: 'oss.show_grid',
|
ossShowGrid: 'oss.show_grid',
|
||||||
|
|
|
@ -4,12 +4,11 @@
|
||||||
* Label is a short text used to represent an entity.
|
* Label is a short text used to represent an entity.
|
||||||
* Description is a long description used in tooltips.
|
* Description is a long description used in tooltips.
|
||||||
*/
|
*/
|
||||||
import { GraphLayout } from '@/components/ui/GraphUI';
|
|
||||||
import { FolderNode } from '@/models/FolderTree';
|
import { FolderNode } from '@/models/FolderTree';
|
||||||
import { GramData, Grammeme, ReferenceType } from '@/models/language';
|
import { GramData, Grammeme, ReferenceType } from '@/models/language';
|
||||||
import { AccessPolicy, LibraryItemType, LocationHead } from '@/models/library';
|
import { AccessPolicy, LibraryItemType, LocationHead } from '@/models/library';
|
||||||
import { validateLocation } from '@/models/libraryAPI';
|
import { validateLocation } from '@/models/libraryAPI';
|
||||||
import { CstMatchMode, DependencyMode, GraphColoring, GraphSizing, HelpTopic } from '@/models/miscellaneous';
|
import { CstMatchMode, DependencyMode, GraphColoring, HelpTopic } from '@/models/miscellaneous';
|
||||||
import { ISubstitutionErrorDescription, OperationType, SubstitutionErrorType } from '@/models/oss';
|
import { ISubstitutionErrorDescription, OperationType, SubstitutionErrorType } from '@/models/oss';
|
||||||
import { CstClass, CstType, ExpressionStatus, IConstituenta, IRSForm } from '@/models/rsform';
|
import { CstClass, CstType, ExpressionStatus, IConstituenta, IRSForm } from '@/models/rsform';
|
||||||
import {
|
import {
|
||||||
|
@ -293,22 +292,6 @@ export function describeLocationHead(head: LocationHead): string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves label for graph layout mode.
|
|
||||||
*/
|
|
||||||
export const mapLabelLayout = new Map<GraphLayout, string>([
|
|
||||||
['treeTd2d', 'Граф: ДеревоВ 2D'],
|
|
||||||
['treeTd3d', 'Граф: ДеревоВ 3D'],
|
|
||||||
['forceatlas2', 'Граф: Атлас 2D'],
|
|
||||||
['forceDirected2d', 'Граф: Силы 2D'],
|
|
||||||
['forceDirected3d', 'Граф: Силы 3D'],
|
|
||||||
['treeLr2d', 'Граф: ДеревоГ 2D'],
|
|
||||||
['treeLr3d', 'Граф: ДеревоГ 3D'],
|
|
||||||
['radialOut2d', 'Граф: Радиус 2D'],
|
|
||||||
['radialOut3d', 'Граф: Радиус 3D'],
|
|
||||||
['circular2d', 'Граф: Круговая']
|
|
||||||
]);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves label for {@link GraphColoring}.
|
* Retrieves label for {@link GraphColoring}.
|
||||||
*/
|
*/
|
||||||
|
@ -319,15 +302,6 @@ export const mapLabelColoring = new Map<GraphColoring, string>([
|
||||||
['schemas', 'Цвет: Схемы']
|
['schemas', 'Цвет: Схемы']
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves label for {@link GraphSizing}.
|
|
||||||
*/
|
|
||||||
export const mapLabelSizing = new Map<GraphSizing, string>([
|
|
||||||
['none', 'Узлы: Моно'],
|
|
||||||
['derived', 'Узлы: Порожденные'],
|
|
||||||
['complex', 'Узлы: Простые']
|
|
||||||
]);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves label for {@link ExpressionStatus}.
|
* Retrieves label for {@link ExpressionStatus}.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -2,32 +2,20 @@
|
||||||
* Module: Mappings for selector UI elements. Do not confuse with html selectors
|
* Module: Mappings for selector UI elements. Do not confuse with html selectors
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { GraphLayout } from '@/components/ui/GraphUI';
|
|
||||||
import { type GramData, Grammeme, ReferenceType } from '@/models/language';
|
import { type GramData, Grammeme, ReferenceType } from '@/models/language';
|
||||||
import { grammemeCompare } from '@/models/languageAPI';
|
import { grammemeCompare } from '@/models/languageAPI';
|
||||||
import { GraphColoring, GraphSizing } from '@/models/miscellaneous';
|
import { GraphColoring } from '@/models/miscellaneous';
|
||||||
import { CstType } from '@/models/rsform';
|
import { CstType } from '@/models/rsform';
|
||||||
|
|
||||||
import { labelGrammeme, labelReferenceType, mapLabelColoring, mapLabelLayout, mapLabelSizing } from './labels';
|
import { labelGrammeme, labelReferenceType, mapLabelColoring } from './labels';
|
||||||
import { labelCstType } from './labels';
|
import { labelCstType } from './labels';
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents options for GraphLayout selector.
|
|
||||||
*/
|
|
||||||
export const SelectorGraphLayout: { value: GraphLayout; label: string }[] = //
|
|
||||||
[...mapLabelLayout.entries()].map(item => ({ value: item[0], label: item[1] }));
|
|
||||||
/**
|
/**
|
||||||
* Represents options for {@link GraphColoring} selector.
|
* Represents options for {@link GraphColoring} selector.
|
||||||
*/
|
*/
|
||||||
export const SelectorGraphColoring: { value: GraphColoring; label: string }[] = //
|
export const SelectorGraphColoring: { value: GraphColoring; label: string }[] = //
|
||||||
[...mapLabelColoring.entries()].map(item => ({ value: item[0], label: item[1] }));
|
[...mapLabelColoring.entries()].map(item => ({ value: item[0], label: item[1] }));
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents options for {@link GraphSizing} selector.
|
|
||||||
*/
|
|
||||||
export const SelectorGraphSizing: { value: GraphSizing; label: string }[] = //
|
|
||||||
[...mapLabelSizing.entries()].map(item => ({ value: item[0], label: item[1] }));
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents options for {@link CstType} selector.
|
* Represents options for {@link CstType} selector.
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in New Issue
Block a user