-
+
+
{ setFilterText(event.target.value); }}
- disabled={onlyExpression}
- />
-
-
- { setOnlyExpression(event.target.checked); }}
/>
+
ids!.find(id => id === cst.id));
+ }
+}
\ No newline at end of file
diff --git a/rsconcept/frontend/src/utils/staticUI.ts b/rsconcept/frontend/src/utils/staticUI.ts
index 4cb812e5..20831121 100644
--- a/rsconcept/frontend/src/utils/staticUI.ts
+++ b/rsconcept/frontend/src/utils/staticUI.ts
@@ -1,7 +1,7 @@
import { LayoutTypes } from 'reagraph';
import { resolveErrorClass,RSErrorClass, RSErrorType, TokenID } from './enums';
-import { CstType, ExpressionStatus, type IConstituenta, IRSErrorDescription,type IRSForm, ISyntaxTreeNode,ParsingStatus, ValueClass } from './models';
+import { CstMatchMode,CstType, DependencyMode,ExpressionStatus, type IConstituenta, IRSErrorDescription,type IRSForm, ISyntaxTreeNode,ParsingStatus, ValueClass } from './models';
export interface IRSButtonData {
text: string
@@ -273,6 +273,27 @@ export const mapLayoutLabels: Map = new Map([
['nooverlap', 'Без перекрытия']
]);
+export function getCstCompareLabel(mode: CstMatchMode): string {
+ switch(mode) {
+ case CstMatchMode.ALL: return 'везде';
+ case CstMatchMode.EXPR: return 'ФВ';
+ case CstMatchMode.TERM: return 'термин';
+ case CstMatchMode.TEXT: return 'текст';
+ case CstMatchMode.NAME: return 'ID';
+ }
+}
+
+export function getDependencyLabel(mode: DependencyMode): string {
+ switch(mode) {
+ case DependencyMode.ALL: return 'вся схема';
+ case DependencyMode.EXPRESSION: return 'выражение';
+ case DependencyMode.OUTPUTS: return 'потребители';
+ case DependencyMode.INPUTS: return 'поставщики';
+ case DependencyMode.EXPAND_INPUTS: return 'влияющие';
+ case DependencyMode.EXPAND_OUTPUTS: return 'зависимые';
+ }
+}
+
export const GraphLayoutSelector: {value: LayoutTypes, label: string}[] = [
{ value: 'forceatlas2', label: 'Атлас 2D'},
{ value: 'forceDirected2d', label: 'Силы 2D'},
From 0c53bf7a303774cefd77d29270e46f42086180a2 Mon Sep 17 00:00:00 2001
From: IRBorisov <8611739+IRBorisov@users.noreply.github.com>
Date: Thu, 3 Aug 2023 16:42:49 +0300
Subject: [PATCH 02/24] Add graph filtering for TermGraph
---
.vscode/launch.json | 22 ++++-
.../src/pages/RSFormPage/EditorTermGraph.tsx | 96 +++++++++++++-----
rsconcept/frontend/src/utils/Graph.test.ts | 45 +++++++++
rsconcept/frontend/src/utils/Graph.ts | 99 ++++++++++++++++---
rsconcept/frontend/src/utils/utils.tsx | 8 ++
5 files changed, 228 insertions(+), 42 deletions(-)
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 99ff9e1f..dcd074d6 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -52,6 +52,26 @@
"request": "launch",
"script": "${workspaceFolder}/rsconcept/RunServer.ps1",
"args": ["-freshStart"]
- }
+ },
+ {
+ "name": "FE-Debug",
+ "type": "node",
+ "request": "launch",
+ "runtimeExecutable": "${workspaceFolder}/rsconcept/frontend/node_modules/.bin/jest",
+ "args": [
+ "${fileBasenameNoExtension}",
+ "--runInBand",
+ "--watch",
+ "--coverage=false",
+ "--no-cache"
+ ],
+ "cwd": "${workspaceFolder}/rsconcept/frontend",
+ "console": "integratedTerminal",
+ "internalConsoleOptions": "neverOpen",
+ "sourceMaps": true,
+ "windows": {
+ "program": "${workspaceFolder}/rsconcept/frontend/node_modules/jest/bin/jest"
+ }
+ },
]
}
\ No newline at end of file
diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph.tsx
index 54f1600c..218c93f0 100644
--- a/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph.tsx
+++ b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph.tsx
@@ -1,35 +1,64 @@
-import { useCallback, useMemo, useRef, useState } from 'react';
-import { darkTheme, GraphCanvas, GraphCanvasRef, GraphEdge, GraphNode, LayoutTypes, lightTheme, useSelection } from 'reagraph';
+import { useCallback, useEffect, 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 { useRSForm } from '../../context/RSFormContext';
import { useConceptTheme } from '../../context/ThemeContext';
import useLocalStorage from '../../hooks/useLocalStorage';
import { resources } from '../../utils/constants';
+import { Graph } from '../../utils/Graph';
import { GraphLayoutSelector,mapLayoutLabels } from '../../utils/staticUI';
function EditorTermGraph() {
const { schema } = useRSForm();
const { darkMode } = useConceptTheme();
const [ layout, setLayout ] = useLocalStorage('graph_layout', 'forceatlas2');
+
+ const [ filtered, setFiltered ] = useState(new Graph());
const [ orbit, setOrbit ] = useState(false);
+ const [ noHermits, setNoHermits ] = useLocalStorage('graph_no_hermits', true);
+ const [ noTransitive, setNoTransitive ] = useLocalStorage('graph_no_transitive', false);
const graphRef = useRef(null);
+ useEffect(() => {
+ if (!schema) {
+ setFiltered(new Graph());
+ return;
+ }
+ const graph = schema.graph.clone();
+ if (noHermits) {
+ graph.removeIsolated();
+ }
+ if (noTransitive) {
+ graph.transitiveReduction();
+ }
+ setFiltered(graph);
+ }, [schema, noHermits, noTransitive]);
+
const nodes: GraphNode[] = useMemo(() => {
- return schema?.items.map(cst => {
- return {
- id: String(cst.id),
- label: (cst.term.resolved || cst.term.raw) ? `${cst.alias}: ${cst.term.resolved || cst.term.raw}` : cst.alias
- }}
- ) ?? [];
- }, [schema?.items]);
+ const result: GraphNode[] = [];
+ if (!schema) {
+ return result;
+ }
+ filtered.nodes.forEach(node => {
+ const cst = schema.items.find(cst => cst.id === node.id);
+ if (cst) {
+ result.push({
+ id: String(node.id),
+ label: cst.term.resolved ? `${cst.alias}: ${cst.term.resolved}` : cst.alias
+ });
+ }
+ });
+ return result;
+ }, [schema, filtered.nodes]);
const edges: GraphEdge[] = useMemo(() => {
const result: GraphEdge[] = [];
let edgeID = 1;
- schema?.graph.nodes.forEach(source => {
+ filtered.nodes.forEach(source => {
source.outputs.forEach(target => {
result.push({
id: String(edgeID),
@@ -40,7 +69,7 @@ function EditorTermGraph() {
});
});
return result;
- }, [schema?.graph]);
+ }, [filtered.nodes]);
const handleCenter = useCallback(() => {
graphRef.current?.resetControls();
@@ -64,22 +93,36 @@ function EditorTermGraph() {
return (<>
-
-
{ setLayout(data.length > 0 ? data[0].value : GraphLayoutSelector[0].value); }}
- />
+
+
+ }
+ dense
+ tooltip='Центрировать изображение'
+ widthClass='h-full'
+ onClick={handleCenter}
+ />
+ { setLayout(data.length > 0 ? data[0].value : GraphLayoutSelector[0].value); }}
+ />
+
setOrbit(event.target.checked) }/>
-
@@ -99,9 +142,12 @@ function EditorTermGraph() {
onNodePointerOver={onNodePointerOver}
onNodePointerOut={onNodePointerOut}
cameraMode={ orbit ? 'orbit' : layout.includes('3d') ? 'rotate' : 'pan'}
- layoutOverrides={ layout.includes('tree') ? { nodeLevelRatio: 1 } : undefined }
+ layoutOverrides={ layout.includes('tree') ? { nodeLevelRatio: 3 } : undefined }
labelFontUrl={resources.graph_font}
theme={darkMode ? darkTheme : lightTheme}
+ renderNode={({ node, ...rest }) => (
+
+ )}
/>
diff --git a/rsconcept/frontend/src/utils/Graph.test.ts b/rsconcept/frontend/src/utils/Graph.test.ts
index 6f69b67d..80861ed8 100644
--- a/rsconcept/frontend/src/utils/Graph.test.ts
+++ b/rsconcept/frontend/src/utils/Graph.test.ts
@@ -20,6 +20,51 @@ describe('Testing Graph constuction', () => {
expect([... graph.nodes.keys()]).toStrictEqual([1, 2, 3, 4]);
expect([... graph.nodes.get(1)!.outputs]).toStrictEqual([2]);
});
+
+ test('cloning', () => {
+ const graph = new Graph([[1, 2], [3], [4, 1]]);
+ const clone = graph.clone();
+ expect([... graph.nodes.keys()]).toStrictEqual([... clone.nodes.keys()]);
+ expect([... graph.nodes.values()]).toStrictEqual([... clone.nodes.values()]);
+
+ clone.removeNode(3);
+ expect(clone.nodes.get(3)).toBeUndefined();
+ expect(graph.nodes.get(3)).not.toBeUndefined();
+ });
+});
+
+describe('Testing Graph editing', () => {
+ test('removing edges should not remove nodes', () => {
+ const graph = new Graph([[1, 2], [3], [4, 1]]);
+ expect(graph.hasEdge(4, 1)).toBeTruthy();
+
+ graph.removeEdge(5, 0);
+ graph.removeEdge(4, 1);
+
+ expect([... graph.nodes.keys()]).toStrictEqual([1, 2, 3, 4]);
+ expect(graph.hasEdge(4, 1)).toBeFalsy();
+ });
+
+ test('removing isolated nodes', () => {
+ const graph = new Graph([[9, 1], [9, 2], [2, 1], [4, 3], [5, 9], [7], [8]]);
+ graph.removeIsolated()
+ expect([... graph.nodes.keys()]).toStrictEqual([9, 1, 2, 4, 3, 5]);
+ });
+
+ test('transitive reduction', () => {
+ const graph = new Graph([[1, 3], [1, 2], [2, 3]]);
+ graph.transitiveReduction()
+ expect(graph.hasEdge(1, 2)).toBeTruthy();
+ expect(graph.hasEdge(2, 3)).toBeTruthy();
+ expect(graph.hasEdge(1, 3)).toBeFalsy();
+ });
+});
+
+describe('Testing Graph sort', () => {
+ test('topological order', () => {
+ const graph = new Graph([[9, 1], [9, 2], [2, 1], [4, 3], [5, 9]]);
+ expect(graph.tolopogicalOrder()).toStrictEqual([5, 4, 3, 9, 1, 2]);
+ });
});
describe('Testing Graph queries', () => {
diff --git a/rsconcept/frontend/src/utils/Graph.ts b/rsconcept/frontend/src/utils/Graph.ts
index 19b00f04..c060c3d9 100644
--- a/rsconcept/frontend/src/utils/Graph.ts
+++ b/rsconcept/frontend/src/utils/Graph.ts
@@ -10,6 +10,13 @@ export class GraphNode {
this.inputs = [];
}
+ clone(): GraphNode {
+ const result = new GraphNode(this.id);
+ result.outputs = [... this.outputs];
+ result.inputs = [... this.inputs];
+ return result;
+ }
+
addOutput(node: number): void {
this.outputs.push(node);
}
@@ -45,6 +52,12 @@ export class Graph {
});
}
+ clone(): Graph {
+ const result = new Graph();
+ this.nodes.forEach(node => result.nodes.set(node.id, node.clone()));
+ return result;
+ }
+
addNode(target: number): GraphNode {
let node = this.nodes.get(target);
if (!node) {
@@ -67,6 +80,16 @@ export class Graph {
return nodeToRemove;
}
+ removeIsolated(): GraphNode[] {
+ const result: GraphNode[] = [];
+ this.nodes.forEach(node => {
+ if (node.outputs.length === 0 && node.inputs.length === 0) {
+ this.nodes.delete(node.id);
+ }
+ });
+ return result;
+ }
+
addEdge(source: number, destination: number): void {
const sourceNode = this.addNode(source);
const destinationNode = this.addNode(destination);
@@ -83,6 +106,14 @@ export class Graph {
}
}
+ hasEdge(source: number, destination: number): boolean {
+ const sourceNode = this.nodes.get(source);
+ if (!sourceNode) {
+ return false;
+ }
+ return !!sourceNode.outputs.find(id => id === destination);
+ }
+
expandOutputs(origin: number[]): number[] {
const result: number[] = [];
const marked = new Map
();
@@ -143,27 +174,63 @@ export class Graph {
return result;
}
- visitDFS(visitor: (node: GraphNode) => void) {
- const visited: Map = new Map();
+ tolopogicalOrder(): number[] {
+ const result: number[] = [];
+ const marked = new Map();
this.nodes.forEach(node => {
- if (!visited.has(node.id)) {
- this.depthFirstSearch(node, visited, visitor);
+ if (marked.get(node.id)) {
+ return;
}
+ const toVisit: number[] = [node.id];
+ let index = 0;
+ while (toVisit.length > 0) {
+ const item = toVisit[index];
+ if (marked.get(item)) {
+ if (!result.find(id => id ===item)) {
+ result.push(item);
+ }
+ toVisit.splice(index, 1);
+ index -= 1;
+ } else {
+ marked.set(item, true);
+ const itemNode = this.nodes.get(item);
+ if (itemNode && itemNode.outputs.length > 0) {
+ itemNode.outputs.forEach(child => {
+ if (!marked.get(child)) {
+ toVisit.push(child);
+ }
+ });
+ }
+ if (index + 1 < toVisit.length) {
+ index += 1;
+ }
+ }
+ }
+ marked
});
+ return result.reverse();
}
- private depthFirstSearch(
- node: GraphNode,
- visited: Map,
- visitor: (node: GraphNode) => void)
- : void {
- visited.set(node.id, true);
- visitor(node);
- node.outputs.forEach((item) => {
- if (!visited.has(item)) {
- const childNode = this.nodes.get(item)!;
- this.depthFirstSearch(childNode, visited, visitor);
+ transitiveReduction() {
+ const order = this.tolopogicalOrder();
+ const marked = new Map();
+ order.forEach(nodeID => {
+ if (marked.get(nodeID)) {
+ return;
+ }
+ const stack: {id: number, parents: number[]}[] = [];
+ stack.push({id: nodeID, parents: []});
+ while (stack.length > 0) {
+ const item = stack.splice(0, 1)[0];
+ const node = this.nodes.get(item.id);
+ if (node) {
+ node.outputs.forEach(child => {
+ item.parents.forEach(parent => this.removeEdge(parent, child));
+ stack.push({id: child, parents: [item.id, ...item.parents]})
+ });
+ }
+ marked.set(item.id, true)
}
});
- }
+ }
}
diff --git a/rsconcept/frontend/src/utils/utils.tsx b/rsconcept/frontend/src/utils/utils.tsx
index 9adcb45c..eb884f99 100644
--- a/rsconcept/frontend/src/utils/utils.tsx
+++ b/rsconcept/frontend/src/utils/utils.tsx
@@ -7,3 +7,11 @@ export function assertIsNode(e: EventTarget | null): asserts e is Node {
export async function delay(ms: number) {
return await new Promise(resolve => setTimeout(resolve, ms));
}
+
+export function trimString(target: string, maxLen: number): string {
+ if (target.length < maxLen) {
+ return target;
+ } else {
+ return target.substring(0, maxLen) + '...';
+ }
+}
\ No newline at end of file
From 6394396beeedb93002fa539a4c5722fac02b139d Mon Sep 17 00:00:00 2001
From: IRBorisov <8611739+IRBorisov@users.noreply.github.com>
Date: Thu, 3 Aug 2023 17:34:55 +0300
Subject: [PATCH 03/24] Fix scroll position
---
rsconcept/frontend/src/App.tsx | 7 ++++++-
.../frontend/src/components/Navigation/Navigation.tsx | 2 +-
rsconcept/frontend/src/pages/RSFormPage/EditorItems.tsx | 2 +-
.../frontend/src/pages/RSFormPage/EditorTermGraph.tsx | 2 +-
4 files changed, 9 insertions(+), 4 deletions(-)
diff --git a/rsconcept/frontend/src/App.tsx b/rsconcept/frontend/src/App.tsx
index ab78ed49..9c702e2c 100644
--- a/rsconcept/frontend/src/App.tsx
+++ b/rsconcept/frontend/src/App.tsx
@@ -3,6 +3,7 @@ import { Route, Routes } from 'react-router-dom';
import Footer from './components/Footer';
import Navigation from './components/Navigation/Navigation';
import ToasterThemed from './components/ToasterThemed';
+import { useConceptTheme } from './context/ThemeContext';
import CreateRSFormPage from './pages/CreateRSFormPage';
import HomePage from './pages/HomePage';
import LibraryPage from './pages/LibraryPage';
@@ -15,6 +16,8 @@ import RSFormPage from './pages/RSFormPage';
import UserProfilePage from './pages/UserProfilePage';
function App () {
+ const { noNavigation } = useConceptTheme();
+
return (
@@ -24,7 +27,8 @@ function App () {
draggable={false}
pauseOnFocusLoss={false}
/>
-
+
+
} />
@@ -42,6 +46,7 @@ function App () {
+
);
}
diff --git a/rsconcept/frontend/src/components/Navigation/Navigation.tsx b/rsconcept/frontend/src/components/Navigation/Navigation.tsx
index a23d79f1..695157c8 100644
--- a/rsconcept/frontend/src/components/Navigation/Navigation.tsx
+++ b/rsconcept/frontend/src/components/Navigation/Navigation.tsx
@@ -18,7 +18,7 @@ function Navigation () {
const navigateHelp = () => { navigate('/manuals') };
return (
-