M: Improve graph UI

This commit is contained in:
Ivan 2024-08-21 20:20:48 +03:00
parent c97dae223b
commit 2083c11ff5
9 changed files with 151 additions and 36 deletions

View File

@ -11,7 +11,15 @@ export default [
...typescriptPlugin.configs.recommendedTypeChecked, ...typescriptPlugin.configs.recommendedTypeChecked,
...typescriptPlugin.configs.stylisticTypeChecked, ...typescriptPlugin.configs.stylisticTypeChecked,
{ {
ignores: ['**/parser.ts', '**/node_modules/**', '**/public/**', 'eslint.config.js'] ignores: [
'**/parser.ts',
'**/node_modules/**',
'**/public/**',
'**/dist/**',
'eslint.config.js',
'tailwind.config.js',
'postcss.config.js'
]
}, },
{ {
languageOptions: { languageOptions: {

View File

@ -119,6 +119,7 @@ export { BiCollapse as IconGraphCollapse } from 'react-icons/bi';
export { BiExpand as IconGraphExpand } from 'react-icons/bi'; export { BiExpand as IconGraphExpand } from 'react-icons/bi';
export { LuMaximize as IconGraphMaximize } from 'react-icons/lu'; export { LuMaximize as IconGraphMaximize } from 'react-icons/lu';
export { BiGitBranch as IconGraphInputs } from 'react-icons/bi'; export { BiGitBranch as IconGraphInputs } from 'react-icons/bi';
export { TbEarScan as IconGraphInverse } from 'react-icons/tb';
export { BiGitMerge as IconGraphOutputs } from 'react-icons/bi'; export { BiGitMerge as IconGraphOutputs } from 'react-icons/bi';
export { LuAtom as IconGraphCore } from 'react-icons/lu'; export { LuAtom as IconGraphCore } from 'react-icons/lu';
export { LuRotate3D as IconRotate3D } from 'react-icons/lu'; export { LuRotate3D as IconRotate3D } from 'react-icons/lu';

View File

@ -82,7 +82,8 @@ function PickMultiConstituenta({ id, schema, prefixID, rows, selected, setSelect
{schema ? ( {schema ? (
<ToolbarGraphSelection <ToolbarGraphSelection
graph={schema.graph} graph={schema.graph}
core={schema.items.filter(cst => isBasicConcept(cst.cst_type)).map(cst => cst.id)} isCore={cstID => isBasicConcept(schema.cstByID.get(cstID)?.cst_type)}
isOwned={cstID => !schema.cstByID.get(cstID)?.is_inherited ?? false}
setSelected={setSelected} setSelected={setSelected}
emptySelection={selected.length === 0} emptySelection={selected.length === 0}
className='w-full ml-8' className='w-full ml-8'

View File

@ -1,4 +1,5 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback } from 'react';
import { Graph } from '@/models/Graph'; import { Graph } from '@/models/Graph';
@ -7,8 +8,10 @@ import {
IconGraphCore, IconGraphCore,
IconGraphExpand, IconGraphExpand,
IconGraphInputs, IconGraphInputs,
IconGraphInverse,
IconGraphMaximize, IconGraphMaximize,
IconGraphOutputs, IconGraphOutputs,
IconPredecessor,
IconReset IconReset
} from '../Icons'; } from '../Icons';
import { CProps } from '../props'; import { CProps } from '../props';
@ -16,7 +19,8 @@ import MiniButton from '../ui/MiniButton';
interface ToolbarGraphSelectionProps extends CProps.Styling { interface ToolbarGraphSelectionProps extends CProps.Styling {
graph: Graph; graph: Graph;
core: number[]; isCore: (item: number) => boolean;
isOwned: (item: number) => boolean;
setSelected: React.Dispatch<React.SetStateAction<number[]>>; setSelected: React.Dispatch<React.SetStateAction<number[]>>;
emptySelection?: boolean; emptySelection?: boolean;
} }
@ -24,11 +28,27 @@ interface ToolbarGraphSelectionProps extends CProps.Styling {
function ToolbarGraphSelection({ function ToolbarGraphSelection({
className, className,
graph, graph,
core, isCore,
isOwned,
setSelected, setSelected,
emptySelection, emptySelection,
...restProps ...restProps
}: ToolbarGraphSelectionProps) { }: ToolbarGraphSelectionProps) {
const handleSelectCore = useCallback(() => {
const core = [...graph.nodes.keys()].filter(isCore);
setSelected([...core, ...graph.expandInputs(core)]);
}, [setSelected, graph, isCore]);
const handleSelectOwned = useCallback(
() => setSelected([...graph.nodes.keys()].filter(isOwned)),
[setSelected, graph, isOwned]
);
const handleInvertSelection = useCallback(
() => setSelected(prev => [...graph.nodes.keys()].filter(item => !prev.includes(item))),
[setSelected, graph]
);
return ( return (
<div className={clsx('cc-icons', className)} {...restProps}> <div className={clsx('cc-icons', className)} {...restProps}>
<MiniButton <MiniButton
@ -67,10 +87,20 @@ function ToolbarGraphSelection({
onClick={() => setSelected(prev => [...prev, ...graph.expandOutputs(prev)])} onClick={() => setSelected(prev => [...prev, ...graph.expandOutputs(prev)])}
disabled={emptySelection} disabled={emptySelection}
/> />
<MiniButton
titleHtml='Инвертировать'
icon={<IconGraphInverse size='1.25rem' className='icon-primary' />}
onClick={handleInvertSelection}
/>
<MiniButton <MiniButton
titleHtml='Выделить ядро' titleHtml='Выделить ядро'
icon={<IconGraphCore size='1.25rem' className='icon-primary' />} icon={<IconGraphCore size='1.25rem' className='icon-primary' />}
onClick={() => setSelected([...core, ...graph.expandInputs(core)])} onClick={handleSelectCore}
/>
<MiniButton
titleHtml='Выделить собственные'
icon={<IconPredecessor size='1.25rem' className='icon-primary' />}
onClick={handleSelectOwned}
/> />
</div> </div>
); );

View File

@ -202,7 +202,7 @@ export function guessCstType(hint: string, defaultType: CstType = CstType.TERM):
/** /**
* Evaluate if {@link CstType} is basic concept. * Evaluate if {@link CstType} is basic concept.
*/ */
export function isBasicConcept(type: CstType): boolean { export function isBasicConcept(type?: CstType): boolean {
// prettier-ignore // prettier-ignore
switch (type) { switch (type) {
case CstType.BASE: return true; case CstType.BASE: return true;
@ -213,6 +213,7 @@ export function isBasicConcept(type: CstType): boolean {
case CstType.FUNCTION: return false; case CstType.FUNCTION: return false;
case CstType.PREDICATE: return false; case CstType.PREDICATE: return false;
case CstType.THEOREM: return false; case CstType.THEOREM: return false;
case undefined: return false;
} }
} }

View File

@ -1,4 +1,4 @@
import { IconRSForm } from '@/components/Icons'; import { IconChild, IconPredecessor, IconRSForm } from '@/components/Icons';
import LinkTopic from '@/components/ui/LinkTopic'; import LinkTopic from '@/components/ui/LinkTopic';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
@ -17,8 +17,8 @@ function HelpThesaurus() {
<h2>Концептуальная схема</h2> <h2>Концептуальная схема</h2>
<p> <p>
<IconRSForm size='1rem' className='inline-icon' />{' '} <IconRSForm size='1rem' className='inline-icon' />{' '}
<LinkTopic text='Концептуальная схема' topic={HelpTopic.CC_SYSTEM} /> (система определений, КС) совокупность <LinkTopic text='Концептуальная схема' topic={HelpTopic.CC_SYSTEM} /> (<i>система определений, КС</i>)
отдельных понятий и утверждений, а также связей между ними, задаваемых определениями. совокупность отдельных понятий и утверждений, а также связей между ними, задаваемых определениями.
</p> </p>
<p> <p>
Экспликация КС изложение (процесс и результат) концептуальной схемы с помощью заданного языка описания Экспликация КС изложение (процесс и результат) концептуальной схемы с помощью заданного языка описания
@ -30,7 +30,82 @@ function HelpThesaurus() {
</p> </p>
<h2>Конституента</h2> <h2>Конституента</h2>
<p>Раздел в разработке...</p> <p>
Конституента это выделенная часть КС, являющаяся отдельным понятием, схемой построения понятия, либо
утверждением, связывающим введенные понятия.{' '}
<LinkTopic text='Аттрибутами конституенты' topic={HelpTopic.CC_CONSTITUENTA} /> в родоструктурной экспликации
являются Термин, Конвенция, Типизация (Структура), Формальное определение, Текстовое определение, Комментарий.
</p>
<ul>
По <b>наличию формального определения в рамках КС</b> выделены:
<li>
базовое понятие (<i>неопределяемое понятие</i>) не имеет определения и задано конвенцией и аксиомами;
</li>
<li>
производное понятие (<i>выводимое понятие</i>) имеет определение.
</li>
</ul>
<br />
<ul>
Для описания <b>тесно связанных понятий</b> введены следующие термины:
<li>
порождающее выражение формальное определение, основанное на одной внешней конституенте и использующее только
формальное разворачивание (не вводит нового предметного содержания);
</li>
<li>основа данного понятия понятие, на котором основано порождающее выражение данной конституенты;</li>
<li>
порожденное понятие данным понятием понятие, определение которого является порождающим выражением,
основанным на данном понятии.
</li>
</ul>
<br />
<ul>
Для описания <b>отождествления</b> введены:
<li>отождествляемые конституенты конституенты, состоящие в отождествлении;</li>
<li>удаляемая конституента конституента, удаляемая в ходе отождествления;</li>
<li>
замещающая конституента конституента, обозначение которой замещает обозначение удаляемой конституенты в
формальных выражениях иных конституент в ходе отождествления;
</li>
</ul>
<br />
<ul>
Для описания <b>наследования</b> конституент в рамках ОСС введены:
<li>
<IconChild size='1rem' className='inline-icon' /> наследованная конституента конституента, перенесенная из
другой КС в рамках операции синтеза;
</li>
<li>собственная конституента конституента, не являющаяся наследником других конституент;</li>
<li>
<IconPredecessor size='1rem' className='inline-icon' /> исходная конституента для данной конституенты
собственная конституента, прямым или опосредованным наследником которой является данная конституента.
</li>
</ul>
<br />
<ul>
По <b>назначению</b> выделены:
<li>
базисное множество (X1) задает неопределяемое понятие, представленное структурой множества, чьи элементы
различимы и не сравнимы с элементами других базисных множеств;
</li>
<li>
константное множество (C1) задает неопределяемое понятие, моделируемое термом теории множеств, который
поддерживает ряд формальных операций над его элементами;
</li>
<li>
родовая структура (S1) задает неопределяемое понятие, имеющее определенную структуру, построенную на базисных
множествах и константных множеств. Содержание родовой структуры формируется{' '}
<LinkTopic text='отношением типизации' topic={HelpTopic.RSL_TYPES} />, аксиомами и конвенцией;
</li>
</ul>
<h2>Операционная схема синтеза</h2> <h2>Операционная схема синтеза</h2>
<p>Раздел в разработке...</p> <p>Раздел в разработке...</p>

View File

@ -37,7 +37,7 @@ function ControlsOverlay({ constituenta, disabled, modified, processing, onRenam
)} )}
> >
<span>Имя </span> <span>Имя </span>
<span className='ml-1'>{constituenta.alias}</span> <span className='ml-1'>{constituenta?.alias ?? ''}</span>
</div> </div>
{!disabled || processing ? ( {!disabled || processing ? (
<MiniButton <MiniButton

View File

@ -324,7 +324,8 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) {
{!focusCst ? ( {!focusCst ? (
<ToolbarGraphSelection <ToolbarGraphSelection
graph={controller.schema!.graph} graph={controller.schema!.graph}
core={controller.schema!.items.filter(cst => isBasicConcept(cst.cst_type)).map(cst => cst.id)} isCore={cstID => isBasicConcept(controller.schema?.cstByID.get(cstID)?.cst_type)}
isOwned={cstID => !controller.schema?.cstByID.get(cstID)?.is_inherited ?? false}
setSelected={controller.setSelected} setSelected={controller.setSelected}
emptySelection={controller.selected.length === 0} emptySelection={controller.selected.length === 0}
/> />

View File

@ -114,30 +114,28 @@ function TermGraph({
const canvasHeight = useMemo(() => calculateHeight('1.75rem + 4px'), [calculateHeight]); const canvasHeight = useMemo(() => calculateHeight('1.75rem + 4px'), [calculateHeight]);
return ( return (
<div className='outline-none'> <div className='relative outline-none' style={{ width: canvasWidth, height: canvasHeight }}>
<div className='relative' style={{ width: canvasWidth, height: canvasHeight }}> <GraphUI
<GraphUI nodes={nodes}
nodes={nodes} edges={edges}
edges={edges} ref={graphRef}
ref={graphRef} animated={false}
animated={false} draggable
draggable layoutType={layout}
layoutType={layout} selections={selections}
selections={selections} onNodeDoubleClick={handleNodeDoubleClick}
onNodeDoubleClick={handleNodeDoubleClick} onNodeClick={handleNodeClick}
onNodeClick={handleNodeClick} onNodePointerOver={handleHoverIn}
onNodePointerOver={handleHoverIn} onNodePointerOut={handleHoverOut}
onNodePointerOut={handleHoverOut} minNodeSize={4}
minNodeSize={4} maxNodeSize={8}
maxNodeSize={8} cameraMode={orbit ? 'orbit' : is3D ? 'rotate' : 'pan'}
cameraMode={orbit ? 'orbit' : is3D ? 'rotate' : 'pan'} layoutOverrides={
layoutOverrides={ layout.includes('tree') ? { nodeLevelRatio: nodes.length < PARAMETER.smallTreeNodes ? 3 : 1 } : undefined
layout.includes('tree') ? { nodeLevelRatio: nodes.length < PARAMETER.smallTreeNodes ? 3 : 1 } : undefined }
} labelFontUrl={resources.graph_font}
labelFontUrl={resources.graph_font} theme={darkMode ? graphDarkT : graphLightT}
theme={darkMode ? graphDarkT : graphLightT} />
/>
</div>
</div> </div>
); );
} }