- {cst.definition?.text.resolved ?? cst.definition?.text.raw ?? ''}
+ const columns = useMemo(
+ () => [
+ {
+ name: 'ID',
+ id: 'id',
+ selector: (cst: IConstituenta) => cst.id,
+ omit: true
+ },
+ {
+ name: 'Имя',
+ id: 'alias',
+ selector: (cst: IConstituenta) => cst.alias,
+ cell: (cst: IConstituenta) => {
+ const info = mapStatusInfo.get(cst.status)!;
+ return (<>
+
+ {cst.alias}
- ),
- minWidth: '200px',
- grow: 2,
- wrap: true,
- reorder: true
+
+ Статус: {info.tooltip}
+
+ >);
},
- {
- name: 'Конвенция / Комментарий',
- id: 'convention',
- cell: (cst: IConstituenta) =>
{cst.convention ?? ''}
,
- minWidth: '100px',
- wrap: true,
- reorder: true,
- hide: 1800
- }
- ], []
- );
+ width: '65px',
+ maxWidth: '65px',
+ reorder: true,
+ },
+ {
+ name: 'Тип',
+ id: 'type',
+ cell: (cst: IConstituenta) =>
{getCstTypificationLabel(cst)}
,
+ width: '175px',
+ maxWidth: '175px',
+ wrap: true,
+ reorder: true,
+ hide: 1600
+ },
+ {
+ name: 'Термин',
+ id: 'term',
+ selector: (cst: IConstituenta) => cst.term?.resolved ?? cst.term?.raw ?? '',
+ width: '350px',
+ minWidth: '150px',
+ maxWidth: '350px',
+ wrap: true,
+ reorder: true
+ },
+ {
+ name: 'Формальное определение',
+ id: 'expression',
+ selector: (cst: IConstituenta) => cst.definition?.formal ?? '',
+ minWidth: '300px',
+ maxWidth: '500px',
+ grow: 2,
+ wrap: true,
+ reorder: true
+ },
+ {
+ name: 'Текстовое определение',
+ id: 'definition',
+ cell: (cst: IConstituenta) => (
+
+ {cst.definition?.text.resolved ?? cst.definition?.text.raw ?? ''}
+
+ ),
+ minWidth: '200px',
+ grow: 2,
+ wrap: true,
+ reorder: true
+ },
+ {
+ name: 'Конвенция / Комментарий',
+ id: 'convention',
+ cell: (cst: IConstituenta) =>
{cst.convention ?? ''}
,
+ minWidth: '100px',
+ wrap: true,
+ reorder: true,
+ hide: 1800
+ }
+ ], []);
return (
@@ -314,20 +314,9 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps)
Клик на квадрат слева - выделение конституенты
Alt + вверх/вниз - движение конституент
Delete - удаление конституент
-
Alt + 1-6, Q,W - добавление конституент
+
Alt + 1-6,Q,W - добавление конституент
-
Статусы
- { [... mapStatusInfo.values()].map(info => {
- return (
-
- {info.text}
-
- -
-
- {info.tooltip}
-
-
);
- })}
+
diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph.tsx
index 13645b8d..781dd72d 100644
--- a/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph.tsx
+++ b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph.tsx
@@ -1,29 +1,117 @@
-import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import { darkTheme, GraphCanvas, GraphCanvasRef, GraphEdge, GraphNode, LayoutTypes, lightTheme, Sphere, useSelection } from 'reagraph';
+import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
+import { darkTheme, GraphCanvas, GraphCanvasRef, GraphEdge,
+ GraphNode, LayoutTypes, lightTheme, Sphere, useSelection
+} from 'reagraph';
import Button from '../../components/Common/Button';
import Checkbox from '../../components/Common/Checkbox';
import ConceptSelect from '../../components/Common/ConceptSelect';
-import { ArrowsRotateIcon } from '../../components/Icons';
+import ConceptTooltip from '../../components/Common/ConceptTooltip';
+import Divider from '../../components/Common/Divider';
+import MiniButton from '../../components/Common/MiniButton';
+import InfoConstituenta from '../../components/Help/InfoConstituenta';
+import InfoCstClass from '../../components/Help/InfoCstClass';
+import CstStatusInfo from '../../components/Help/InfoCstStatus';
+import { ArrowsRotateIcon, DumpBinIcon, FilterCogIcon, HelpIcon, SmallPlusIcon } from '../../components/Icons';
import { useRSForm } from '../../context/RSFormContext';
import { useConceptTheme } from '../../context/ThemeContext';
import useLocalStorage from '../../hooks/useLocalStorage';
-import { resources } from '../../utils/constants';
+import { prefixes, resources } from '../../utils/constants';
import { Graph } from '../../utils/Graph';
-import { GraphLayoutSelector,mapLayoutLabels } from '../../utils/staticUI';
+import { CstType, IConstituenta } from '../../utils/models';
+import { getCstClassColor, getCstStatusColor,
+ GraphColoringSelector, GraphLayoutSelector,
+ mapColoringLabels, mapLayoutLabels
+} from '../../utils/staticUI';
+import DlgGraphOptions from './DlgGraphOptions';
+import ConstituentaTooltip from './elements/ConstituentaTooltip';
-function EditorTermGraph() {
- const { schema } = useRSForm();
+export type ColoringScheme = 'none' | 'status' | 'type';
+const TREE_SIZE_MILESTONE = 50;
+
+function getCstNodeColor(cst: IConstituenta, coloringScheme: ColoringScheme, darkMode: boolean): string {
+ if (coloringScheme === 'type') {
+ return getCstClassColor(cst.cstClass, darkMode);
+ }
+ if (coloringScheme === 'status') {
+ return getCstStatusColor(cst.status, darkMode);
+ }
+ return (darkMode ? '#7a8c9e' :'#7ca0ab');
+}
+
+export interface GraphEditorParams {
+ noHermits: boolean
+ noTransitive: boolean
+ noTemplates: boolean
+ noTerms: boolean
+
+ allowBase: boolean
+ allowStruct: boolean
+ allowTerm: boolean
+ allowAxiom: boolean
+ allowFunction: boolean
+ allowPredicate: boolean
+ allowConstant: boolean
+ allowTheorem: boolean
+}
+
+interface EditorTermGraphProps {
+ onOpenEdit: (cstID: number) => void
+ onCreateCst: (selectedID: number | undefined, type: CstType | undefined, skipDialog?: boolean) => void
+ onDeleteCst: (selected: number[], callback: (items: number[]) => void) => void
+}
+
+function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGraphProps) {
+ const { schema, isEditable } = useRSForm();
const { darkMode, noNavigation } = useConceptTheme();
+
const [ layout, setLayout ] = useLocalStorage
('graph_layout', 'treeTd2d');
-
- const [ filtered, setFiltered ] = useState(new Graph());
+ const [ coloringScheme, setColoringScheme ] = useLocalStorage('graph_coloring', 'none');
const [ orbit, setOrbit ] = useState(false);
+
const [ noHermits, setNoHermits ] = useLocalStorage('graph_no_hermits', true);
const [ noTransitive, setNoTransitive ] = useLocalStorage('graph_no_transitive', false);
+ const [ noTemplates, setNoTemplates ] = useLocalStorage('graph_no_templates', false);
+ const [ noTerms, setNoTerms ] = useLocalStorage('graph_no_terms', false);
+ const [ allowBase, setAllowBase ] = useLocalStorage('graph_allow_base', true);
+ const [ allowStruct, setAllowStruct ] = useLocalStorage('graph_allow_struct', true);
+ const [ allowTerm, setAllowTerm ] = useLocalStorage('graph_allow_term', true);
+ const [ allowAxiom, setAllowAxiom ] = useLocalStorage('graph_allow_axiom', true);
+ const [ allowFunction, setAllowFunction ] = useLocalStorage('function', true);
+ const [ allowPredicate, setAllowPredicate ] = useLocalStorage('graph_allow_predicate', true);
+ const [ allowConstant, setAllowConstant ] = useLocalStorage('graph_allow_constant', true);
+ const [ allowTheorem, setAllowTheorem ] = useLocalStorage('graph_allow_theorem', true);
+
+ const [ filtered, setFiltered ] = useState(new Graph());
+ const [ dismissed, setDismissed ] = useState([]);
+ const [ selectedDismissed, setSelectedDismissed ] = useState([]);
const graphRef = useRef(null);
+ const [showOptions, setShowOptions] = useState(false);
+ const [toggleUpdate, setToggleUpdate] = useState(false);
+
+ const [hoverID, setHoverID] = useState(undefined);
+ const hoverCst = useMemo(
+ () => {
+ return schema?.items.find(cst => String(cst.id) == hoverID);
+ }, [schema?.items, hoverID]);
- useEffect(() => {
+ const is3D = useMemo(() => layout.includes('3d'), [layout]);
+ const allowedTypes: CstType[] = useMemo(
+ () => {
+ const result: CstType[] = [];
+ if (allowBase) result.push(CstType.BASE);
+ if (allowStruct) result.push(CstType.STRUCTURED);
+ if (allowTerm) result.push(CstType.TERM);
+ if (allowAxiom) result.push(CstType.AXIOM);
+ if (allowFunction) result.push(CstType.FUNCTION);
+ if (allowPredicate) result.push(CstType.PREDICATE);
+ if (allowConstant) result.push(CstType.CONSTANT);
+ if (allowTheorem) result.push(CstType.THEOREM);
+ return result;
+ }, [allowBase, allowStruct, allowTerm, allowAxiom, allowFunction, allowPredicate, allowConstant, allowTheorem]);
+
+ useLayoutEffect(
+ () => {
if (!schema) {
setFiltered(new Graph());
return;
@@ -35,10 +123,46 @@ function EditorTermGraph() {
if (noTransitive) {
graph.transitiveReduction();
}
+ if (noTemplates) {
+ schema.items.forEach(cst => {
+ if (cst.isTemplate) {
+ graph.foldNode(cst.id);
+ }
+ });
+ }
+ if (allowedTypes.length < Object.values(CstType).length) {
+ schema.items.forEach(cst => {
+ if (!allowedTypes.includes(cst.cstType)) {
+ graph.foldNode(cst.id);
+ }
+ });
+ }
+ const newDismissed: number[] = [];
+ schema.items.forEach(cst => {
+ if (!graph.nodes.has(cst.id)) {
+ newDismissed.push(cst.id);
+ }
+ });
setFiltered(graph);
- }, [schema, noHermits, noTransitive]);
+ setDismissed(newDismissed);
+ setSelectedDismissed([]);
+ setHoverID(undefined);
+ }, [schema, noHermits, noTransitive, noTemplates, allowedTypes, toggleUpdate]);
- const nodes: GraphNode[] = useMemo(() => {
+ function toggleDismissed(cstID: number) {
+ setSelectedDismissed(prev => {
+ const index = prev.findIndex(id => cstID == id);
+ if (index !== -1) {
+ prev.splice(index, 1);
+ } else {
+ prev.push(cstID);
+ }
+ return [... prev];
+ });
+ }
+
+ const nodes: GraphNode[] = useMemo(
+ () => {
const result: GraphNode[] = [];
if (!schema) {
return result;
@@ -48,14 +172,16 @@ function EditorTermGraph() {
if (cst) {
result.push({
id: String(node.id),
- label: cst.term.resolved ? `${cst.alias}: ${cst.term.resolved}` : cst.alias
+ fill: getCstNodeColor(cst, coloringScheme, darkMode),
+ label: cst.term.resolved && !noTerms ? `${cst.alias}: ${cst.term.resolved}` : cst.alias
});
}
});
return result;
- }, [schema, filtered.nodes]);
+ }, [schema, coloringScheme, filtered.nodes, darkMode, noTerms]);
- const edges: GraphEdge[] = useMemo(() => {
+ const edges: GraphEdge[] = useMemo(
+ () => {
const result: GraphEdge[] = [];
let edgeID = 1;
filtered.nodes.forEach(source => {
@@ -70,15 +196,11 @@ function EditorTermGraph() {
});
return result;
}, [filtered.nodes]);
-
- const handleCenter = useCallback(() => {
- graphRef.current?.resetControls();
- graphRef.current?.centerGraph();
- }, []);
-
+
const {
selections, actives,
onNodeClick,
+ clearSelections,
onCanvasClick,
onNodePointerOver,
onNodePointerOut
@@ -87,52 +209,292 @@ function EditorTermGraph() {
nodes,
edges,
type: 'multi', // 'single' | 'multi' | 'multiModifier'
- pathSelectionType: 'all',
+ pathSelectionType: 'out',
+ pathHoverType: 'all',
focusOnSelect: false
});
- const canvasSize = !noNavigation ?
- 'w-[1240px] h-[736px] 2xl:w-[1880px] 2xl:h-[746px]'
- : 'w-[1240px] h-[800px] 2xl:w-[1880px] 2xl:h-[810px]';
+ const allSelected: string[] = useMemo(
+ () => {
+ return [ ... selectedDismissed.map(id => String(id)), ... selections];
+ }, [selectedDismissed, selections]);
+ const nothingSelected = useMemo(() => allSelected.length === 0, [allSelected]);
- return (<>
-
-
-
+ const handleRecreate = useCallback(
+ () => {
+ graphRef.current?.resetControls();
+ graphRef.current?.centerGraph();
+ }, []);
+
+ const handleHoverIn = useCallback(
+ (node: GraphNode) => {
+ setHoverID(node.id);
+ if (onNodePointerOver) onNodePointerOver(node);
+ }, [onNodePointerOver]);
+
+ const handleHoverOut = useCallback(
+ (node: GraphNode) => {
+ setHoverID(undefined);
+ if (onNodePointerOut) onNodePointerOut(node);
+ }, [onNodePointerOut]);
+
+ const handleNodeClick = useCallback(
+ (node: GraphNode) => {
+ if (selections.includes(node.id)) {
+ onOpenEdit(Number(node.id));
+ return;
+ }
+ if (onNodeClick) onNodeClick(node);
+ }, [onNodeClick, selections, onOpenEdit]);
+
+ const handleCanvasClick = useCallback(
+ (event: MouseEvent) => {
+ setSelectedDismissed([]);
+ if (onCanvasClick) onCanvasClick(event);
+ }, [onCanvasClick]);
+
+ // Implement hotkeys for editing
+ function handleKeyDown(event: React.KeyboardEvent
) {
+ console.log(event);
+ if (!isEditable) {
+ return;
+ }
+ if (event.key === 'Delete' && allSelected.length > 0) {
+ event.preventDefault();
+ handleDeleteCst();
+ return;
+ }
+ }
+
+ function handleCreateCst() {
+ if (!schema) {
+ return;
+ }
+ const selectedPosition = allSelected.reduce((prev, cstID) => {
+ const position = schema.items.findIndex(cst => cst.id === Number(cstID));
+ return Math.max(position, prev);
+ }, -1);
+ const insert_where = selectedPosition >= 0 ? schema.items[selectedPosition].id : undefined;
+ onCreateCst(insert_where, undefined);
+ }
+
+ function handleDeleteCst() {
+ if (!schema) {
+ return;
+ }
+ onDeleteCst([... allSelected.map(id => Number(id))], () => {
+ clearSelections();
+ setDismissed([]);
+ setSelectedDismissed([]);
+ setToggleUpdate(prev => !prev);
+ });
+ }
+
+ function getOptions() {
+ return {
+ noHermits: noHermits,
+ noTemplates: noTemplates,
+ noTransitive: noTransitive,
+ noTerms: noTerms,
+
+ allowBase: allowBase,
+ allowStruct: allowStruct,
+ allowTerm: allowTerm,
+ allowAxiom: allowAxiom,
+ allowFunction: allowFunction,
+ allowPredicate: allowPredicate,
+ allowConstant: allowConstant,
+ allowTheorem: allowTheorem
+ }
+ }
+
+ const handleChangeOptions = useCallback(
+ (params: GraphEditorParams) => {
+ setNoHermits(params.noHermits);
+ setNoTransitive(params.noTransitive);
+ setNoTemplates(params.noTemplates);
+ setNoTerms(params.noTerms);
+
+ setAllowBase(params.allowBase);
+ setAllowStruct(params.allowStruct);
+ setAllowTerm(params.allowTerm);
+ setAllowAxiom(params.allowAxiom);
+ setAllowFunction(params.allowFunction);
+ setAllowPredicate(params.allowPredicate);
+ setAllowConstant(params.allowConstant);
+ setAllowTheorem(params.allowTheorem);
+ }, [setNoHermits, setNoTransitive, setNoTemplates,
+ setAllowBase, setAllowStruct, setAllowTerm, setAllowAxiom, setAllowFunction,
+ setAllowPredicate, setAllowConstant, setAllowTheorem, setNoTerms]);
+
+ const canvasWidth = useMemo(
+ () => {
+ return 'calc(100vw - 14.6rem)';
+ }, []);
+
+ const canvasHeight = useMemo(
+ () => {
+ return !noNavigation ?
+ 'calc(100vh - 13rem)'
+ : 'calc(100vh - 2rem)';
+ }, [noNavigation]);
+
+ const dismissedStyle = useCallback(
+ (cstID: number) => {
+ return selectedDismissed.includes(cstID) ? {outlineWidth: '2px', outlineStyle: 'solid'}: {};
+ }, [selectedDismissed]);
+
+ return (
+
+ {showOptions &&
+
setShowOptions(false)}
+ initial={getOptions()}
+ onConfirm={handleChangeOptions}
+ />}
+
+ {hoverCst &&
+
+
+
}
+
+
+
+ Выбраны
+
+ {allSelected.length} из {schema?.stats?.count_all ?? 0}
+
+
+
+ }
+ disabled={!isEditable || nothingSelected}
+ onClick={handleDeleteCst}
+ />
+ }
+ disabled={!isEditable}
+ onClick={handleCreateCst}
+ />
+
+
+
}
+ icon={ }
dense
- tooltip='Центрировать изображение'
+ tooltip='Настройки фильтрации узлов и связей'
widthClass='h-full'
- onClick={handleCenter}
+ onClick={() => setShowOptions(true)}
/>
{ setLayout(data.length > 0 ? data[0].value : GraphLayoutSelector[0].value); }}
+ className='min-w-[9.3rem]'
+ options={GraphColoringSelector}
+ searchable={false}
+ placeholder='Выберите цвет'
+ values={coloringScheme ? [{ value: coloringScheme, label: mapColoringLabels.get(coloringScheme) }] : []}
+ onChange={data => { setColoringScheme(data.length > 0 ? data[0].value : GraphColoringSelector[0].value); }}
/>
+
-
setOrbit(event.target.checked) }
+ { setLayout(data.length > 0 ? data[0].value : GraphLayoutSelector[0].value); }}
/>
setNoHermits(event.target.checked) }
+ label='Скрыть текст'
+ value={noTerms}
+ onChange={ event => setNoTerms(event.target.checked) }
/>
setNoTransitive(event.target.checked) }
/>
+ setOrbit(event.target.checked) }
+ />
+
+
+
+
+
Скрытые конституенты
+
+ {dismissed.map(cstID => {
+ const cst = schema!.items.find(cst => cst.id === cstID)!;
+ const adjustedColoring = coloringScheme === 'none' ? 'status': coloringScheme;
+ return (<>
+
toggleDismissed(cstID)}
+ onDoubleClick={() => onOpenEdit(cstID)}
+ >
+ {cst.alias}
+
+
+ >);
+ })}
+
+
-
-
-
+
+
+
+
+
+
+
}
+ tooltip='Пересоздать граф'
+ onClick={handleRecreate}
+ />
+
+
+
+
+
Настройка графа
+
Цвет - выбор правила покраски узлов
+
Граф - выбор модели расположения узлов
+
Удалить несвязанные - скрыть одинокие вершины
+
Транзитивная редукция - скрыть транзитивные пути
+
+
+
+
+
+
+
+
Горячие клавиши
+
Клик на конституенту - выделение, включая скрытые конституенты
+
Довйной клик - редактирование конституенты
+
Delete - удалить выбранные
+
+
+
+
+
+
+
(
@@ -156,7 +520,7 @@ function EditorTermGraph() {
/>
- >);
+
);
}
diff --git a/rsconcept/frontend/src/pages/RSFormPage/RSTabs.tsx b/rsconcept/frontend/src/pages/RSFormPage/RSTabs.tsx
index 87e673de..959835a1 100644
--- a/rsconcept/frontend/src/pages/RSFormPage/RSTabs.tsx
+++ b/rsconcept/frontend/src/pages/RSFormPage/RSTabs.tsx
@@ -40,7 +40,7 @@ function RSTabs() {
const { destroySchema } = useLibrary();
const [activeTab, setActiveTab] = useState(RSTabsList.CARD);
- const [activeID, setActiveID] = useState
(undefined)
+ const [activeID, setActiveID] = useState(undefined);
const [showUpload, setShowUpload] = useState(false);
const [showClone, setShowClone] = useState(false);
@@ -223,7 +223,7 @@ function RSTabs() {
defaultFocus={true}
selectedTabClassName='font-bold'
>
-
+
setShowClone(true)}
@@ -264,7 +264,11 @@ function RSTabs() {
-
+
>}
diff --git a/rsconcept/frontend/src/pages/RSFormPage/elements/ConstituentaTooltip.tsx b/rsconcept/frontend/src/pages/RSFormPage/elements/ConstituentaTooltip.tsx
index 286483dd..0fc442f9 100644
--- a/rsconcept/frontend/src/pages/RSFormPage/elements/ConstituentaTooltip.tsx
+++ b/rsconcept/frontend/src/pages/RSFormPage/elements/ConstituentaTooltip.tsx
@@ -1,6 +1,6 @@
import ConceptTooltip from '../../../components/Common/ConceptTooltip';
+import InfoConstituenta from '../../../components/Help/InfoConstituenta';
import { IConstituenta } from '../../../utils/models';
-import { getCstTypificationLabel } from '../../../utils/staticUI';
interface ConstituentaTooltipProps {
data: IConstituenta
@@ -13,12 +13,7 @@ function ConstituentaTooltip({ data, anchor }: ConstituentaTooltipProps) {
anchorSelect={anchor}
className='max-w-[25rem] min-w-[25rem]'
>
- Конституента {data.alias}
- Типизация: {getCstTypificationLabel(data)}
- Термин: {data.term.resolved || data.term.raw}
- {data.definition.formal && Выражение: {data.definition.formal}
}
- {data.definition.text.resolved && Определение: {data.definition.text.resolved}
}
- {data.convention && Конвенция: {data.convention}
}
+
);
}
diff --git a/rsconcept/frontend/src/pages/RSFormPage/elements/StatusBar.tsx b/rsconcept/frontend/src/pages/RSFormPage/elements/StatusBar.tsx
index 6769fc69..f2b49c72 100644
--- a/rsconcept/frontend/src/pages/RSFormPage/elements/StatusBar.tsx
+++ b/rsconcept/frontend/src/pages/RSFormPage/elements/StatusBar.tsx
@@ -24,7 +24,7 @@ function StatusBar({ isModified, constituenta, parseData }: StatusBarProps) {
const data = mapStatusInfo.get(status)!;
return (
+ className={`text-sm h-[1.6rem] w-[10rem] font-semibold inline-flex border items-center select-none justify-center align-middle ${data.color}`}>
Статус: [ {data.text} ]
)
diff --git a/rsconcept/frontend/src/pages/RSFormPage/elements/ViewSideConstituents.tsx b/rsconcept/frontend/src/pages/RSFormPage/elements/ViewSideConstituents.tsx
index 00e5727c..5c215574 100644
--- a/rsconcept/frontend/src/pages/RSFormPage/elements/ViewSideConstituents.tsx
+++ b/rsconcept/frontend/src/pages/RSFormPage/elements/ViewSideConstituents.tsx
@@ -158,7 +158,7 @@ function ViewSideConstituents({ expression, baseHeight, activeID, onOpenEdit }:
onChange={setFilterMatch}
/>
setFilterText(event.target.value)}
diff --git a/rsconcept/frontend/src/utils/Graph.test.ts b/rsconcept/frontend/src/utils/Graph.test.ts
index 80861ed8..063b42f4 100644
--- a/rsconcept/frontend/src/utils/Graph.test.ts
+++ b/rsconcept/frontend/src/utils/Graph.test.ts
@@ -45,6 +45,16 @@ describe('Testing Graph editing', () => {
expect(graph.hasEdge(4, 1)).toBeFalsy();
});
+ test('folding node redirectes edges', () => {
+ const graph = new Graph([[1, 3], [2, 3], [3, 4], [3, 5], [3, 3]]);
+ graph.foldNode(3);
+ expect(graph.hasNode(3)).toBeFalsy();
+ expect(graph.hasEdge(1, 4)).toBeTruthy();
+ expect(graph.hasEdge(1, 5)).toBeTruthy();
+ expect(graph.hasEdge(2, 4)).toBeTruthy();
+ expect(graph.hasEdge(2, 5)).toBeTruthy();
+ });
+
test('removing isolated nodes', () => {
const graph = new Graph([[9, 1], [9, 2], [2, 1], [4, 3], [5, 9], [7], [8]]);
graph.removeIsolated()
@@ -53,7 +63,7 @@ describe('Testing Graph editing', () => {
test('transitive reduction', () => {
const graph = new Graph([[1, 3], [1, 2], [2, 3]]);
- graph.transitiveReduction()
+ graph.transitiveReduction();
expect(graph.hasEdge(1, 2)).toBeTruthy();
expect(graph.hasEdge(2, 3)).toBeTruthy();
expect(graph.hasEdge(1, 3)).toBeFalsy();
diff --git a/rsconcept/frontend/src/utils/Graph.ts b/rsconcept/frontend/src/utils/Graph.ts
index c060c3d9..4e873e6d 100644
--- a/rsconcept/frontend/src/utils/Graph.ts
+++ b/rsconcept/frontend/src/utils/Graph.ts
@@ -67,6 +67,10 @@ export class Graph {
return node;
}
+ hasNode(target: number): boolean {
+ return !!this.nodes.get(target);
+ }
+
removeNode(target: number): GraphNode | null {
const nodeToRemove = this.nodes.get(target);
if (!nodeToRemove) {
@@ -80,6 +84,19 @@ export class Graph {
return nodeToRemove;
}
+ foldNode(target: number): GraphNode | null {
+ const nodeToRemove = this.nodes.get(target);
+ if (!nodeToRemove) {
+ return null;
+ }
+ nodeToRemove.inputs.forEach(input => {
+ nodeToRemove.outputs.forEach(output => {
+ this.addEdge(input, output);
+ })
+ });
+ return this.removeNode(target);
+ }
+
removeIsolated(): GraphNode[] {
const result: GraphNode[] = [];
this.nodes.forEach(node => {
diff --git a/rsconcept/frontend/src/utils/constants.ts b/rsconcept/frontend/src/utils/constants.ts
index 452a0ad8..eb598700 100644
--- a/rsconcept/frontend/src/utils/constants.ts
+++ b/rsconcept/frontend/src/utils/constants.ts
@@ -28,5 +28,6 @@ export const resources = {
}
export const prefixes = {
- cst_list: 'cst-list-'
+ cst_list: 'cst-list-',
+ cst_status_list: 'cst-status-list-'
}
diff --git a/rsconcept/frontend/src/utils/models.ts b/rsconcept/frontend/src/utils/models.ts
index c26f2214..62a7e228 100644
--- a/rsconcept/frontend/src/utils/models.ts
+++ b/rsconcept/frontend/src/utils/models.ts
@@ -101,6 +101,13 @@ export enum CstType {
THEOREM = 'theorem'
}
+export enum CstClass {
+ BASIC = 'basic',
+ DERIVED = 'derived',
+ STATEMENT = 'statement',
+ TEMPLATE = 'template'
+}
+
export interface IConstituenta {
id: number
alias: string
@@ -118,7 +125,9 @@ export interface IConstituenta {
resolved: string
}
}
+ cstClass: CstClass
status: ExpressionStatus
+ isTemplate: boolean
parse: {
status: ParsingStatus
valueClass: ValueClass
@@ -230,12 +239,12 @@ export enum EditMode {
// RSExpression status
export enum ExpressionStatus {
- UNDEFINED = 0,
- UNKNOWN,
- INCORRECT,
- INCALCULABLE,
- PROPERTY,
- VERIFIED
+ UNDEFINED = 'undefined',
+ UNKNOWN = 'unknown',
+ INCORRECT = 'incorrect',
+ INCALCULABLE = 'incalculable',
+ PROPERTY = 'property',
+ VERIFIED = 'verified'
}
// Dependency mode for schema analysis
@@ -274,7 +283,28 @@ export function inferStatus(parse?: ParsingStatus, value?: ValueClass): Expressi
if (value === ValueClass.PROPERTY) {
return ExpressionStatus.PROPERTY;
}
- return ExpressionStatus.VERIFIED
+ return ExpressionStatus.VERIFIED;
+}
+
+export function inferTemplate(expression: string): boolean {
+ const match = expression.match(/R\d+/g);
+ return (match && match?.length > 0) ?? false;
+}
+
+export function inferClass(type: CstType, isTemplate: boolean): CstClass {
+ if (isTemplate) {
+ return CstClass.TEMPLATE;
+ }
+ switch (type) {
+ case CstType.BASE: return CstClass.BASIC;
+ case CstType.CONSTANT: return CstClass.BASIC;
+ case CstType.STRUCTURED: return CstClass.BASIC;
+ case CstType.TERM: return CstClass.DERIVED;
+ case CstType.FUNCTION: return CstClass.DERIVED;
+ case CstType.AXIOM: return CstClass.STATEMENT;
+ case CstType.PREDICATE: return CstClass.DERIVED;
+ case CstType.THEOREM: return CstClass.STATEMENT;
+ }
}
export function extractGlobals(expression: string): Set {
@@ -336,6 +366,8 @@ export function LoadRSFormData(schema: IRSFormData): IRSForm {
}
result.items.forEach(cst => {
cst.status = inferStatus(cst.parse.status, cst.parse.valueClass);
+ cst.isTemplate = inferTemplate(cst.definition.formal);
+ cst.cstClass = inferClass(cst.cstType, cst.isTemplate);
result.graph.addNode(cst.id);
const dependencies = extractGlobals(cst.definition.formal);
dependencies.forEach(value => {
diff --git a/rsconcept/frontend/src/utils/staticUI.ts b/rsconcept/frontend/src/utils/staticUI.ts
index ba884892..97ee8dde 100644
--- a/rsconcept/frontend/src/utils/staticUI.ts
+++ b/rsconcept/frontend/src/utils/staticUI.ts
@@ -1,7 +1,8 @@
import { LayoutTypes } from 'reagraph';
+import { ColoringScheme } from '../pages/RSFormPage/EditorTermGraph';
import { resolveErrorClass,RSErrorClass, RSErrorType, TokenID } from './enums';
-import { CstMatchMode, CstType, DependencyMode,ExpressionStatus, IConstituenta,
+import { CstClass, CstMatchMode, CstType, DependencyMode, ExpressionStatus, IConstituenta,
IFunctionArg,IRSErrorDescription, IRSForm,
ISyntaxTreeNode, ParsingStatus, ValueClass
} from './models';
@@ -11,7 +12,7 @@ export interface IRSButtonData {
tooltip: string
}
-export interface IStatusInfo {
+export interface IFormatInfo {
text: string
color: string
tooltip: string
@@ -250,22 +251,6 @@ export const CstTypeSelector = (Object.values(CstType)).map(
return { value: type, label: getCstTypeLabel(type) };
});
-export const mapLayoutLabels: Map = new Map([
- ['forceatlas2', 'Атлас 2D'],
- ['forceDirected2d', 'Силы 2D'],
- ['forceDirected3d', 'Силы 3D'],
- ['treeTd2d', 'ДеревоВерт 2D'],
- ['treeTd3d', 'ДеревоВерт 3D'],
- ['treeLr2d', 'ДеревоГор 2D'],
- ['treeLr3d', 'ДеревоГор 3D'],
- ['radialOut2d', 'Радиальная 2D'],
- ['radialOut3d', 'Радиальная 3D'],
- ['circular2d', 'Круговая'],
- ['hierarchicalTd', 'ИерархияВерт'],
- ['hierarchicalLr', 'ИерархияГор'],
- ['nooverlap', 'Без перекрытия']
-]);
-
export function getCstCompareLabel(mode: CstMatchMode): string {
switch(mode) {
case CstMatchMode.ALL: return 'везде';
@@ -288,22 +273,61 @@ export function getDependencyLabel(mode: DependencyMode): string {
}
export const GraphLayoutSelector: {value: LayoutTypes, label: string}[] = [
- { value: 'forceatlas2', label: 'Атлас 2D'},
- { value: 'forceDirected2d', label: 'Силы 2D'},
- { value: 'forceDirected3d', label: 'Силы 3D'},
- { value: 'treeTd2d', label: 'ДеревоВ 2D'},
- { value: 'treeTd3d', label: 'ДеревоВ 3D'},
- { value: 'treeLr2d', label: 'ДеревоГ 2D'},
- { value: 'treeLr3d', label: 'ДеревоГ 3D'},
- { value: 'radialOut2d', label: 'Радиальная 2D'},
- { value: 'radialOut3d', label: 'Радиальная 3D'},
+ { value: 'treeTd2d', label: 'Граф: ДеревоВ 2D'},
+ { value: 'treeTd3d', label: 'Граф: ДеревоВ 3D'},
+ { value: 'forceatlas2', label: 'Граф: Атлас 2D'},
+ { value: 'forceDirected2d', label: 'Граф: Силы 2D'},
+ { value: 'forceDirected3d', label: 'Граф: Силы 3D'},
+ { value: 'treeLr2d', label: 'Граф: ДеревоГ 2D'},
+ { value: 'treeLr3d', label: 'Граф: ДеревоГ 3D'},
+ { value: 'radialOut2d', label: 'Граф: Радиальная 2D'},
+ { value: 'radialOut3d', label: 'Граф: Радиальная 3D'},
// { value: 'circular2d', label: 'circular2d'},
// { value: 'nooverlap', label: 'nooverlap'},
// { value: 'hierarchicalTd', label: 'hierarchicalTd'},
// { value: 'hierarchicalLr', label: 'hierarchicalLr'}
];
-export const mapStatusInfo: Map = new Map([
+export const mapLayoutLabels: Map = new Map([
+ ['forceatlas2', 'Граф: Атлас 2D'],
+ ['forceDirected2d', 'Граф: Силы 2D'],
+ ['forceDirected3d', 'Граф: Силы 3D'],
+ ['treeTd2d', 'Граф: ДеревоВерт 2D'],
+ ['treeTd3d', 'Граф: ДеревоВерт 3D'],
+ ['treeLr2d', 'Граф: ДеревоГор 2D'],
+ ['treeLr3d', 'Граф: ДеревоГор 3D'],
+ ['radialOut2d', 'Граф: Радиальная 2D'],
+ ['radialOut3d', 'Граф: Радиальная 3D'],
+ ['circular2d', 'Граф: Круговая'],
+ ['hierarchicalTd', 'Граф: ИерархияВерт'],
+ ['hierarchicalLr', 'Граф: ИерархияГор'],
+ ['nooverlap', 'Граф: Без перекрытия']
+]);
+
+export const mapColoringLabels: Map = new Map([
+ ['none', 'Цвет: моно'],
+ ['status', 'Цвет: статус'],
+ ['type', 'Цвет: класс'],
+]);
+
+export const GraphColoringSelector: {value: ColoringScheme, label: string}[] = [
+ { value: 'none', label: 'Цвет: моно'},
+ { value: 'status', label: 'Цвет: статус'},
+ { value: 'type', label: 'Цвет: класс'},
+];
+
+export function getCstStatusColor(status: ExpressionStatus, darkMode: boolean): string {
+ switch (status) {
+ case ExpressionStatus.VERIFIED: return darkMode ? '#2b8000': '#aaff80';
+ case ExpressionStatus.INCORRECT: return darkMode ? '#592b2b': '#ffc9c9';
+ case ExpressionStatus.INCALCULABLE: return darkMode ? '#964600': '#ffbb80';
+ case ExpressionStatus.PROPERTY: return darkMode ? '#36899e': '#a5e9fa';
+ case ExpressionStatus.UNKNOWN: return darkMode ? '#1e00b3': '#b3bdff';
+ case ExpressionStatus.UNDEFINED: return darkMode ? '#1e00b3': '#b3bdff';
+ }
+}
+
+export const mapStatusInfo: Map = new Map([
[ ExpressionStatus.VERIFIED, {
text: 'ок',
color: 'bg-[#aaff80] dark:bg-[#2b8000]',
@@ -317,7 +341,7 @@ export const mapStatusInfo: Map = new Map([
[ ExpressionStatus.INCALCULABLE, {
text: 'невыч',
color: 'bg-[#ffbb80] dark:bg-[#964600]',
- tooltip: 'выражение не вычислимо (экспоненциальная сложность)'
+ tooltip: 'выражение не вычислимо'
}],
[ ExpressionStatus.PROPERTY, {
text: 'св-во',
@@ -336,6 +360,38 @@ export const mapStatusInfo: Map = new Map([
}],
]);
+export function getCstClassColor(cstClass: CstClass, darkMode: boolean): string {
+ switch (cstClass) {
+ case CstClass.TEMPLATE: return darkMode ? '#36899e': '#a5e9fa';
+ case CstClass.BASIC: return darkMode ? '#2b8000': '#aaff80';
+ case CstClass.DERIVED: return darkMode ? '#1e00b3': '#b3bdff';
+ case CstClass.STATEMENT: return darkMode ? '#592b2b': '#ffc9c9';
+ }
+}
+
+export const mapCstClassInfo: Map = new Map([
+ [ CstClass.BASIC, {
+ text: 'базовый',
+ color: 'bg-[#aaff80] dark:bg-[#2b8000]',
+ tooltip: 'неопределяемое понятие, требует конвенции'
+ }],
+ [ CstClass.DERIVED, {
+ text: 'производный',
+ color: 'bg-[#b3bdff] dark:bg-[#1e00b3]',
+ tooltip: 'выводимое понятие, задаваемое определением'
+ }],
+ [ CstClass.STATEMENT, {
+ text: 'утверждение',
+ color: 'bg-[#ffc9c9] dark:bg-[#592b2b]',
+ tooltip: 'неопределяемое понятие, требует конвенции'
+ }],
+ [ CstClass.TEMPLATE, {
+ text: 'шаблон',
+ color: 'bg-[#a5e9fa] dark:bg-[#36899e]',
+ tooltip: 'параметризованный шаблон определения'
+ }],
+]);
+
export function createAliasFor(type: CstType, schema: IRSForm): string {
const prefix = getCstTypePrefix(type);
if (!schema.items || schema.items.length <= 0) {
@@ -370,6 +426,8 @@ export function getMockConstituenta(id: number, alias: string, type: CstType, co
}
},
status: ExpressionStatus.INCORRECT,
+ isTemplate: false,
+ cstClass: CstClass.DERIVED,
parse: {
status: ParsingStatus.INCORRECT,
valueClass: ValueClass.INVALID,