mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 04:50:36 +03:00
F: Term graph rework pt1
This commit is contained in:
parent
2b09044e8f
commit
23f2e142cf
|
@ -44,7 +44,6 @@ This readme file is used mostly to document project dependencies and conventions
|
|||
- js-file-download
|
||||
- use-debounce
|
||||
- framer-motion
|
||||
- reagraph
|
||||
- html-to-image
|
||||
- @tanstack/react-table
|
||||
- @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-zoom-pan-pinch": "^3.6.1",
|
||||
"reactflow": "^11.11.4",
|
||||
"reagraph": "^4.20.1",
|
||||
"use-debounce": "^10.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -139,6 +139,7 @@ function PickMultiConstituenta({
|
|||
graph={foldedGraph}
|
||||
isCore={cstID => isBasicConcept(schema.cstByID.get(cstID)?.cst_type)}
|
||||
isOwned={cstID => !schema.cstByID.get(cstID)?.is_inherited}
|
||||
selected={selected}
|
||||
setSelected={setSelected}
|
||||
emptySelection={selected.length === 0}
|
||||
className='w-fit'
|
||||
|
|
|
@ -19,15 +19,17 @@ import MiniButton from '../ui/MiniButton';
|
|||
|
||||
interface ToolbarGraphSelectionProps extends CProps.Styling {
|
||||
graph: Graph;
|
||||
selected: number[];
|
||||
isCore: (item: number) => boolean;
|
||||
isOwned: (item: number) => boolean;
|
||||
setSelected: React.Dispatch<React.SetStateAction<number[]>>;
|
||||
isOwned?: (item: number) => boolean;
|
||||
setSelected: (newSelection: number[]) => void;
|
||||
emptySelection?: boolean;
|
||||
}
|
||||
|
||||
function ToolbarGraphSelection({
|
||||
className,
|
||||
graph,
|
||||
selected,
|
||||
isCore,
|
||||
isOwned,
|
||||
setSelected,
|
||||
|
@ -40,13 +42,13 @@ function ToolbarGraphSelection({
|
|||
}, [setSelected, graph, isCore]);
|
||||
|
||||
const handleSelectOwned = useCallback(
|
||||
() => setSelected([...graph.nodes.keys()].filter(isOwned)),
|
||||
() => (isOwned ? setSelected([...graph.nodes.keys()].filter(isOwned)) : undefined),
|
||||
[setSelected, graph, isOwned]
|
||||
);
|
||||
|
||||
const handleInvertSelection = useCallback(
|
||||
() => setSelected(prev => [...graph.nodes.keys()].filter(item => !prev.includes(item))),
|
||||
[setSelected, graph]
|
||||
() => setSelected([...graph.nodes.keys()].filter(item => !selected.includes(item))),
|
||||
[setSelected, selected, graph]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -60,31 +62,31 @@ function ToolbarGraphSelection({
|
|||
<MiniButton
|
||||
titleHtml='Выделить все влияющие'
|
||||
icon={<IconGraphCollapse size='1.25rem' className='icon-primary' />}
|
||||
onClick={() => setSelected(prev => [...prev, ...graph.expandAllInputs(prev)])}
|
||||
onClick={() => setSelected([...selected, ...graph.expandAllInputs(selected)])}
|
||||
disabled={emptySelection}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml='Выделить все зависимые'
|
||||
icon={<IconGraphExpand size='1.25rem' className='icon-primary' />}
|
||||
onClick={() => setSelected(prev => [...prev, ...graph.expandAllOutputs(prev)])}
|
||||
onClick={() => setSelected([...selected, ...graph.expandAllOutputs(selected)])}
|
||||
disabled={emptySelection}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml='<b>Максимизация</b> <br/>дополнение выделения конституентами, <br/>зависимыми только от выделенных'
|
||||
icon={<IconGraphMaximize size='1.25rem' className='icon-primary' />}
|
||||
onClick={() => setSelected(prev => graph.maximizePart(prev))}
|
||||
onClick={() => setSelected(graph.maximizePart(selected))}
|
||||
disabled={emptySelection}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml='Выделить поставщиков'
|
||||
icon={<IconGraphInputs size='1.25rem' className='icon-primary' />}
|
||||
onClick={() => setSelected(prev => [...prev, ...graph.expandInputs(prev)])}
|
||||
onClick={() => setSelected([...selected, ...graph.expandInputs(selected)])}
|
||||
disabled={emptySelection}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml='Выделить потребителей'
|
||||
icon={<IconGraphOutputs size='1.25rem' className='icon-primary' />}
|
||||
onClick={() => setSelected(prev => [...prev, ...graph.expandOutputs(prev)])}
|
||||
onClick={() => setSelected([...selected, ...graph.expandOutputs(selected)])}
|
||||
disabled={emptySelection}
|
||||
/>
|
||||
<MiniButton
|
||||
|
@ -97,11 +99,13 @@ function ToolbarGraphSelection({
|
|||
icon={<IconGraphCore size='1.25rem' className='icon-primary' />}
|
||||
onClick={handleSelectCore}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml='Выделить собственные'
|
||||
icon={<IconPredecessor size='1.25rem' className='icon-primary' />}
|
||||
onClick={handleSelectOwned}
|
||||
/>
|
||||
{isOwned ? (
|
||||
<MiniButton
|
||||
titleHtml='Выделить собственные'
|
||||
icon={<IconPredecessor size='1.25rem' className='icon-primary' />}
|
||||
onClick={handleSelectOwned}
|
||||
/>
|
||||
) : null}
|
||||
</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';
|
||||
|
||||
/**
|
||||
* Represents graph node sizing scheme.
|
||||
*/
|
||||
export type GraphSizing = 'none' | 'complex' | 'derived';
|
||||
|
||||
/**
|
||||
* Represents manuals topic.
|
||||
*/
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
*/
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
|
||||
import { DependencyMode, GraphSizing, Position2D } from './miscellaneous';
|
||||
import { DependencyMode, Position2D } from './miscellaneous';
|
||||
import { IOperationPosition, IOperationSchema, OperationID, OperationType } from './oss';
|
||||
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}
|
||||
*/
|
||||
|
|
|
@ -32,8 +32,6 @@ function HelpRSGraphTerm() {
|
|||
<div className='sm:w-[14rem]'>
|
||||
<h1>Настройка графа</h1>
|
||||
<li>Цвет – покраска узлов</li>
|
||||
<li>Граф – расположение</li>
|
||||
<li>Размер – размер узлов</li>
|
||||
<li>
|
||||
<IconText className='inline-icon' /> Отображение текста
|
||||
</li>
|
||||
|
@ -51,7 +49,7 @@ function HelpRSGraphTerm() {
|
|||
<h1>Изменение узлов</h1>
|
||||
<li>Клик на конституенту – выделение</li>
|
||||
<li>
|
||||
Ctrl + клик – выбор <span style={{ color: colors.fgPurple }}>фокус-конституенты</span>
|
||||
Alt + клик – выбор <span style={{ color: colors.fgPurple }}>фокус-конституенты</span>
|
||||
</li>
|
||||
<li>
|
||||
<IconReset className='inline-icon' /> Esc – сбросить выделение
|
||||
|
|
|
@ -32,6 +32,9 @@ import { OssNodeTypes } from './graph/OssNodeTypes';
|
|||
import NodeContextMenu, { ContextMenuData } from './NodeContextMenu';
|
||||
import ToolbarOssGraph from './ToolbarOssGraph';
|
||||
|
||||
const ZOOM_MAX = 2;
|
||||
const ZOOM_MIN = 0.5;
|
||||
|
||||
interface OssFlowProps {
|
||||
isModified: boolean;
|
||||
setIsModified: (newValue: boolean) => void;
|
||||
|
@ -223,7 +226,7 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
|||
const imageWidth = PARAMETER.ossImageWidth;
|
||||
const imageHeight = PARAMETER.ossImageHeight;
|
||||
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, {
|
||||
backgroundColor: colors.bgDefault,
|
||||
width: imageWidth,
|
||||
|
@ -266,7 +269,7 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
|||
setMenuProps(undefined);
|
||||
}, [controller]);
|
||||
|
||||
const handleClickCanvas = useCallback(() => {
|
||||
const handleCanvasClick = useCallback(() => {
|
||||
handleContextMenuHide();
|
||||
}, [handleContextMenuHide]);
|
||||
|
||||
|
@ -322,13 +325,13 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
|||
nodesFocusable={false}
|
||||
fitView
|
||||
nodeTypes={OssNodeTypes}
|
||||
maxZoom={2}
|
||||
minZoom={0.5}
|
||||
maxZoom={ZOOM_MAX}
|
||||
minZoom={ZOOM_MIN}
|
||||
nodesConnectable={false}
|
||||
snapToGrid={true}
|
||||
snapGrid={[PARAMETER.ossGridSize, PARAMETER.ossGridSize]}
|
||||
onNodeContextMenu={handleContextMenu}
|
||||
onClick={handleClickCanvas}
|
||||
onClick={handleCanvasClick}
|
||||
>
|
||||
{showGrid ? <Background gap={PARAMETER.ossGridSize} /> : null}
|
||||
</ReactFlow>
|
||||
|
@ -338,7 +341,7 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
|||
edges,
|
||||
handleNodesChange,
|
||||
handleContextMenu,
|
||||
handleClickCanvas,
|
||||
handleCanvasClick,
|
||||
onEdgesChange,
|
||||
handleNodeDoubleClick,
|
||||
showGrid
|
||||
|
|
|
@ -1,390 +1,18 @@
|
|||
'use client';
|
||||
import { ReactFlowProvider } from 'reactflow';
|
||||
|
||||
import clsx from 'clsx';
|
||||
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 { ConstituentaID } from '@/models/rsform';
|
||||
|
||||
import InfoConstituenta from '@/components/info/InfoConstituenta';
|
||||
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';
|
||||
import TGFlow from './TGFlow';
|
||||
|
||||
interface EditorTermGraphProps {
|
||||
onOpenEdit: (cstID: ConstituentaID) => void;
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
<AnimatePresence>
|
||||
{showParamsDialog ? (
|
||||
<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>
|
||||
</>
|
||||
<ReactFlowProvider>
|
||||
<TGFlow onOpenEdit={onOpenEdit} />
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,57 +1,34 @@
|
|||
import BadgeHelp from '@/components/info/BadgeHelp';
|
||||
import { GraphLayout } from '@/components/ui/GraphUI';
|
||||
import Overlay from '@/components/ui/Overlay';
|
||||
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 { mapLabelColoring, mapLabelLayout, mapLabelSizing } from '@/utils/labels';
|
||||
import { SelectorGraphColoring, SelectorGraphLayout, SelectorGraphSizing } from '@/utils/selectors';
|
||||
import { mapLabelColoring } from '@/utils/labels';
|
||||
import { SelectorGraphColoring } from '@/utils/selectors';
|
||||
|
||||
import SchemasGuide from './SchemasGuide';
|
||||
|
||||
interface GraphSelectorsProps {
|
||||
schema?: IRSForm;
|
||||
coloring: GraphColoring;
|
||||
layout: GraphLayout;
|
||||
sizing: GraphSizing;
|
||||
|
||||
setLayout: (newValue: GraphLayout) => void;
|
||||
setColoring: (newValue: GraphColoring) => void;
|
||||
setSizing: (newValue: GraphSizing) => void;
|
||||
onChangeColoring: (newValue: GraphColoring) => void;
|
||||
}
|
||||
|
||||
function GraphSelectors({ schema, coloring, setColoring, layout, setLayout, sizing, setSizing }: GraphSelectorsProps) {
|
||||
function GraphSelectors({ schema, coloring, onChangeColoring }: GraphSelectorsProps) {
|
||||
return (
|
||||
<div className='border rounded-b-none select-none clr-input rounded-t-md'>
|
||||
<SelectSingle
|
||||
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]'>
|
||||
<Overlay position='right-[2.5rem] top-[0.25rem]'>
|
||||
{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 === 'schemas' && !!schema ? <SchemasGuide schema={schema} /> : null}
|
||||
</Overlay>
|
||||
<SelectSingle
|
||||
className='my-1'
|
||||
noBorder
|
||||
placeholder='Цветовая схема'
|
||||
options={SelectorGraphColoring}
|
||||
isSearchable={false}
|
||||
value={coloring ? { value: coloring, label: mapLabelColoring.get(coloring) } : null}
|
||||
onChange={data => setColoring(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)}
|
||||
onChange={data => onChangeColoring(data?.value ?? SelectorGraphColoring[0].value)}
|
||||
/>
|
||||
</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,
|
||||
IconImage,
|
||||
IconNewItem,
|
||||
IconRotate3D,
|
||||
IconText,
|
||||
IconTextOff,
|
||||
IconTypeGraph
|
||||
|
@ -22,9 +21,6 @@ import { PARAMETER } from '@/utils/constants';
|
|||
import { useRSEdit } from '../RSEditContext';
|
||||
|
||||
interface ToolbarTermGraphProps {
|
||||
is3D: boolean;
|
||||
|
||||
orbit: boolean;
|
||||
noText: boolean;
|
||||
foldDerived: boolean;
|
||||
|
||||
|
@ -36,17 +32,13 @@ interface ToolbarTermGraphProps {
|
|||
|
||||
toggleFoldDerived: () => void;
|
||||
toggleNoText: () => void;
|
||||
toggleOrbit: () => void;
|
||||
}
|
||||
|
||||
function ToolbarTermGraph({
|
||||
is3D,
|
||||
noText,
|
||||
foldDerived,
|
||||
toggleNoText,
|
||||
toggleFoldDerived,
|
||||
orbit,
|
||||
toggleOrbit,
|
||||
showParamsDialog,
|
||||
onCreate,
|
||||
onDelete,
|
||||
|
@ -95,12 +87,6 @@ function ToolbarTermGraph({
|
|||
}
|
||||
onClick={toggleFoldDerived}
|
||||
/>
|
||||
<MiniButton
|
||||
icon={<IconRotate3D size='1.25rem' className={orbit ? 'icon-green' : 'icon-primary'} />}
|
||||
title='Анимация вращения'
|
||||
disabled={!is3D}
|
||||
onClick={toggleOrbit}
|
||||
/>
|
||||
{controller.isContentEditable ? (
|
||||
<MiniButton
|
||||
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;
|
||||
bgPrimary: string;
|
||||
bgSelected: string;
|
||||
bgActiveSelection: string;
|
||||
bgHover: string;
|
||||
bgWarning: string;
|
||||
|
||||
|
@ -62,6 +63,7 @@ export const lightT: IColorTheme = {
|
|||
bgDisabled: 'var(--cl-bg-60)',
|
||||
bgPrimary: 'var(--cl-prim-bg-100)',
|
||||
bgSelected: 'var(--cl-prim-bg-80)',
|
||||
bgActiveSelection: 'var(--cl-teal-bg-100)',
|
||||
bgHover: 'var(--cl-prim-bg-60)',
|
||||
bgWarning: 'var(--cl-red-bg-100)',
|
||||
|
||||
|
@ -104,6 +106,7 @@ export const darkT: IColorTheme = {
|
|||
bgDisabled: 'var(--cd-bg-60)',
|
||||
bgPrimary: 'var(--cd-prim-bg-100)',
|
||||
bgSelected: 'var(--cd-prim-bg-80)',
|
||||
bgActiveSelection: 'var(--cd-teal-bg-100)',
|
||||
bgHover: 'var(--cd-prim-bg-60)',
|
||||
bgWarning: 'var(--cd-red-bg-100)',
|
||||
|
||||
|
@ -184,96 +187,6 @@ export const selectDarkT = {
|
|||
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.
|
||||
*/
|
||||
|
|
|
@ -38,6 +38,8 @@
|
|||
--cl-red-fg-100: hsl(000, 072%, 051%);
|
||||
--cl-green-fg-100: hsl(120, 080%, 37%);
|
||||
|
||||
--cl-teal-bg-100: hsl(162, 082%, 051%);
|
||||
|
||||
/* Dark Theme */
|
||||
--cd-bg-120: hsl(000, 000%, 005%);
|
||||
--cd-bg-100: hsl(000, 000%, 009%);
|
||||
|
@ -59,4 +61,6 @@
|
|||
--cd-red-bg-100: hsl(000, 100%, 015%);
|
||||
--cd-red-fg-100: hsl(000, 080%, 055%);
|
||||
--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-token {
|
||||
.react-flow__node-token,
|
||||
.react-flow__node-concept {
|
||||
cursor: default;
|
||||
|
||||
border-radius: 100%;
|
||||
width: 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
|
||||
refreshTimeout: 100, // milliseconds delay for post-refresh actions
|
||||
minimalTimeout: 10, // milliseconds delay for fast updates
|
||||
|
||||
zoomDuration: 500, // milliseconds animation duration
|
||||
|
||||
ossImageWidth: 1280, // pixels - size of OSS image
|
||||
ossImageHeight: 960, // pixels - size of OSS image
|
||||
ossContextMenuWidth: 200, // pixels - width of OSS context menu
|
||||
|
@ -21,16 +21,16 @@ export const PARAMETER = {
|
|||
ossDistanceX: 180, // pixels - insert x-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
|
||||
graphHoverYLimit: 0.6, // ratio to clientHeight used to determine which side of screen popup should be
|
||||
graphPopupDelay: 500, // milliseconds delay for graph popup selections
|
||||
graphRefreshDelay: 10, // milliseconds delay for graph viewpoint reset
|
||||
|
||||
typificationTruncate: 42, // characters - threshold for long typification - truncate
|
||||
|
||||
ossLongLabel: 14, // characters - threshold for long labels - small font
|
||||
ossTruncateLabel: 32, // characters - threshold for long labels - truncate
|
||||
|
||||
statSmallThreshold: 3, // characters - threshold for small labels - small font
|
||||
|
||||
logicLabel: 'LOGIC',
|
||||
|
@ -121,9 +121,7 @@ export const storage = {
|
|||
libraryPagination: 'library.pagination',
|
||||
|
||||
rsgraphFilter: 'rsgraph.filter2',
|
||||
rsgraphLayout: 'rsgraph.layout',
|
||||
rsgraphColoring: 'rsgraph.coloring',
|
||||
rsgraphSizing: 'rsgraph.sizing',
|
||||
rsgraphFoldHidden: 'rsgraph.fold_hidden',
|
||||
|
||||
ossShowGrid: 'oss.show_grid',
|
||||
|
|
|
@ -4,12 +4,11 @@
|
|||
* Label is a short text used to represent an entity.
|
||||
* Description is a long description used in tooltips.
|
||||
*/
|
||||
import { GraphLayout } from '@/components/ui/GraphUI';
|
||||
import { FolderNode } from '@/models/FolderTree';
|
||||
import { GramData, Grammeme, ReferenceType } from '@/models/language';
|
||||
import { AccessPolicy, LibraryItemType, LocationHead } from '@/models/library';
|
||||
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 { CstClass, CstType, ExpressionStatus, IConstituenta, IRSForm } from '@/models/rsform';
|
||||
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}.
|
||||
*/
|
||||
|
@ -319,15 +302,6 @@ export const mapLabelColoring = new Map<GraphColoring, string>([
|
|||
['schemas', 'Цвет: Схемы']
|
||||
]);
|
||||
|
||||
/**
|
||||
* Retrieves label for {@link GraphSizing}.
|
||||
*/
|
||||
export const mapLabelSizing = new Map<GraphSizing, string>([
|
||||
['none', 'Узлы: Моно'],
|
||||
['derived', 'Узлы: Порожденные'],
|
||||
['complex', 'Узлы: Простые']
|
||||
]);
|
||||
|
||||
/**
|
||||
* Retrieves label for {@link ExpressionStatus}.
|
||||
*/
|
||||
|
|
|
@ -2,32 +2,20 @@
|
|||
* 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 { grammemeCompare } from '@/models/languageAPI';
|
||||
import { GraphColoring, GraphSizing } from '@/models/miscellaneous';
|
||||
import { GraphColoring } from '@/models/miscellaneous';
|
||||
import { CstType } from '@/models/rsform';
|
||||
|
||||
import { labelGrammeme, labelReferenceType, mapLabelColoring, mapLabelLayout, mapLabelSizing } from './labels';
|
||||
import { labelGrammeme, labelReferenceType, mapLabelColoring } 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.
|
||||
*/
|
||||
export const SelectorGraphColoring: { value: GraphColoring; label: string }[] = //
|
||||
[...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.
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue
Block a user