mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 21:10:38 +03:00
F: Implementing block UI pt1
This commit is contained in:
parent
30d2a0b2ff
commit
12fffd0087
|
@ -10,6 +10,7 @@ export { BiCheck as IconAccept } from 'react-icons/bi';
|
|||
export { BiX as IconRemove } from 'react-icons/bi';
|
||||
export { BiTrash as IconDestroy } from 'react-icons/bi';
|
||||
export { BiReset as IconReset } from 'react-icons/bi';
|
||||
export { TbArrowsDiagonal2 as IconResize } from 'react-icons/tb';
|
||||
export { LiaEdit as IconEdit } from 'react-icons/lia';
|
||||
export { FiEdit as IconEdit2 } from 'react-icons/fi';
|
||||
export { BiSearchAlt2 as IconSearch } from 'react-icons/bi';
|
||||
|
@ -72,6 +73,7 @@ export { BiBot as IconRobot } from 'react-icons/bi';
|
|||
export { IoLibrary as IconLibrary2 } from 'react-icons/io5';
|
||||
export { BiDiamond as IconTemplates } from 'react-icons/bi';
|
||||
export { TbHexagons as IconOSS } from 'react-icons/tb';
|
||||
export { BiScreenshot as IconConceptBlock } from 'react-icons/bi';
|
||||
export { TbHexagon as IconRSForm } from 'react-icons/tb';
|
||||
export { TbAssembly as IconRSFormOwned } from 'react-icons/tb';
|
||||
export { TbBallFootball as IconRSFormImported } from 'react-icons/tb';
|
||||
|
|
|
@ -6,18 +6,20 @@ import { type ILibraryItem } from '@/features/library';
|
|||
|
||||
import { Graph } from '@/models/graph';
|
||||
|
||||
import { type IOperation, type IOperationSchema, type IOperationSchemaStats } from '../models/oss';
|
||||
import { type IBlock, type IOperation, type IOperationSchema, type IOperationSchemaStats } from '../models/oss';
|
||||
|
||||
import { type IOperationSchemaDTO, OperationType } from './types';
|
||||
|
||||
/**
|
||||
* Loads data into an {@link IOperationSchema} based on {@link IOperationSchemaDTO}.
|
||||
*
|
||||
*/
|
||||
export const DEFAULT_BLOCK_WIDTH = 100;
|
||||
export const DEFAULT_BLOCK_HEIGHT = 100;
|
||||
|
||||
/** Loads data into an {@link IOperationSchema} based on {@link IOperationSchemaDTO}. */
|
||||
export class OssLoader {
|
||||
private oss: IOperationSchema;
|
||||
private graph: Graph = new Graph();
|
||||
private hierarchy: Graph = new Graph();
|
||||
private operationByID = new Map<number, IOperation>();
|
||||
private blockByID = new Map<number, IBlock>();
|
||||
private schemaIDs: number[] = [];
|
||||
private items: ILibraryItem[];
|
||||
|
||||
|
@ -32,9 +34,12 @@ export class OssLoader {
|
|||
this.createGraph();
|
||||
this.extractSchemas();
|
||||
this.inferOperationAttributes();
|
||||
this.inferBlockAttributes();
|
||||
|
||||
result.operationByID = this.operationByID;
|
||||
result.blockByID = this.blockByID;
|
||||
result.graph = this.graph;
|
||||
result.hierarchy = this.hierarchy;
|
||||
result.schemas = this.schemaIDs;
|
||||
result.stats = this.calculateStats();
|
||||
return result;
|
||||
|
@ -44,6 +49,17 @@ export class OssLoader {
|
|||
this.oss.operations.forEach(operation => {
|
||||
this.operationByID.set(operation.id, operation);
|
||||
this.graph.addNode(operation.id);
|
||||
this.hierarchy.addNode(operation.id);
|
||||
if (operation.parent) {
|
||||
this.hierarchy.addEdge(-operation.parent, operation.id);
|
||||
}
|
||||
});
|
||||
this.oss.blocks.forEach(block => {
|
||||
this.blockByID.set(block.id, block);
|
||||
this.hierarchy.addNode(-block.id);
|
||||
if (block.parent) {
|
||||
this.graph.addEdge(-block.parent, -block.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -71,6 +87,16 @@ export class OssLoader {
|
|||
});
|
||||
}
|
||||
|
||||
private inferBlockAttributes() {
|
||||
this.oss.blocks.forEach(block => {
|
||||
const geometry = this.oss.layout.blocks.find(item => item.id === block.id);
|
||||
block.x = geometry?.x ?? 0;
|
||||
block.y = geometry?.y ?? 0;
|
||||
block.width = geometry?.width ?? DEFAULT_BLOCK_WIDTH;
|
||||
block.height = geometry?.height ?? DEFAULT_BLOCK_HEIGHT;
|
||||
});
|
||||
}
|
||||
|
||||
private inferConsolidation(operationID: number): boolean {
|
||||
const inputs = this.graph.expandInputs([operationID]);
|
||||
if (inputs.length === 0) {
|
||||
|
@ -85,13 +111,14 @@ export class OssLoader {
|
|||
}
|
||||
|
||||
private calculateStats(): IOperationSchemaStats {
|
||||
const items = this.oss.operations;
|
||||
const operations = this.oss.operations;
|
||||
return {
|
||||
count_operations: items.length,
|
||||
count_inputs: items.filter(item => item.operation_type === OperationType.INPUT).length,
|
||||
count_synthesis: items.filter(item => item.operation_type === OperationType.SYNTHESIS).length,
|
||||
count_all: this.oss.operations.length + this.oss.blocks.length,
|
||||
count_inputs: operations.filter(item => item.operation_type === OperationType.INPUT).length,
|
||||
count_synthesis: operations.filter(item => item.operation_type === OperationType.SYNTHESIS).length,
|
||||
count_schemas: this.schemaIDs.length,
|
||||
count_owned: items.filter(item => !!item.result && item.is_owned).length
|
||||
count_owned: operations.filter(item => !!item.result && item.is_owned).length,
|
||||
count_block: this.oss.blocks.length
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ export type ICstSubstituteInfo = z.infer<typeof schemaCstSubstituteInfo>;
|
|||
/** Represents {@link IOperation} data from server. */
|
||||
export type IOperationDTO = z.infer<typeof schemaOperation>;
|
||||
|
||||
/** Represents {@link IOperation} data from server. */
|
||||
/** Represents {@link IBlock} data from server. */
|
||||
export type IBlockDTO = z.infer<typeof schemaBlock>;
|
||||
|
||||
/** Represents backend data for {@link IOperationSchema}. */
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
/**
|
||||
* Module: OSS representation.
|
||||
* Module: OSS graphical representation.
|
||||
*/
|
||||
import { type Node } from 'reactflow';
|
||||
|
||||
import { type IOperation } from './oss';
|
||||
import { type IBlock, type IOperation } from './oss';
|
||||
|
||||
/**
|
||||
* Represents XY Position.
|
||||
|
@ -13,9 +13,7 @@ export interface Position2D {
|
|||
y: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents graph OSS node data.
|
||||
*/
|
||||
/** Represents graph OSS node data. */
|
||||
export interface OssNode extends Node {
|
||||
id: string;
|
||||
data: {
|
||||
|
@ -25,15 +23,27 @@ export interface OssNode extends Node {
|
|||
position: { x: number; y: number };
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents graph OSS node internal data.
|
||||
*/
|
||||
export interface OssNodeInternal {
|
||||
/** Represents graph OSS node internal data for {@link IOperation}. */
|
||||
export interface OperationInternalNode {
|
||||
id: string;
|
||||
data: {
|
||||
label: string;
|
||||
operation: IOperation;
|
||||
};
|
||||
selected: boolean;
|
||||
dragging: boolean;
|
||||
xPos: number;
|
||||
yPos: number;
|
||||
}
|
||||
|
||||
/** Represents graph OSS node internal data for {@link IBlock}. */
|
||||
export interface BlockInternalNode {
|
||||
id: string;
|
||||
data: {
|
||||
label: string;
|
||||
block: IBlock;
|
||||
};
|
||||
selected: boolean;
|
||||
dragging: boolean;
|
||||
xPos: number;
|
||||
yPos: number;
|
||||
|
|
|
@ -4,7 +4,12 @@
|
|||
|
||||
import { type Graph } from '@/models/graph';
|
||||
|
||||
import { type ICstSubstituteInfo, type IOperationDTO, type IOperationSchemaDTO } from '../backend/types';
|
||||
import {
|
||||
type IBlockDTO,
|
||||
type ICstSubstituteInfo,
|
||||
type IOperationDTO,
|
||||
type IOperationSchemaDTO
|
||||
} from '../backend/types';
|
||||
|
||||
/** Represents Operation. */
|
||||
export interface IOperation extends IOperationDTO {
|
||||
|
@ -16,23 +21,35 @@ export interface IOperation extends IOperationDTO {
|
|||
arguments: number[];
|
||||
}
|
||||
|
||||
/** Represents Block. */
|
||||
export interface IBlock extends IBlockDTO {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
/** Represents {@link IOperationSchema} statistics. */
|
||||
export interface IOperationSchemaStats {
|
||||
count_operations: number;
|
||||
count_all: number;
|
||||
count_inputs: number;
|
||||
count_synthesis: number;
|
||||
count_schemas: number;
|
||||
count_owned: number;
|
||||
count_block: number;
|
||||
}
|
||||
|
||||
/** Represents OperationSchema. */
|
||||
export interface IOperationSchema extends IOperationSchemaDTO {
|
||||
operations: IOperation[];
|
||||
blocks: IBlock[];
|
||||
|
||||
graph: Graph;
|
||||
hierarchy: Graph;
|
||||
schemas: number[];
|
||||
stats: IOperationSchemaStats;
|
||||
operationByID: Map<number, IOperation>;
|
||||
blockByID: Map<number, IBlock>;
|
||||
}
|
||||
|
||||
/** Represents substitution error description. */
|
||||
|
|
|
@ -54,7 +54,7 @@ export function EditorOssCard() {
|
|||
<EditorLibraryItem schema={schema} isAttachedToOSS={false} />
|
||||
</div>
|
||||
|
||||
<OssStats className='mt-3 md:mt-8 md:ml-5 w-56 md:w-48 mx-auto h-min' stats={schema.stats} />
|
||||
<OssStats className='mt-3 md:mt-8 md:ml-5 w-80 md:w-56 mx-auto h-min' stats={schema.stats} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -1,4 +1,11 @@
|
|||
import { IconDownload, IconRSForm, IconRSFormImported, IconRSFormOwned, IconSynthesis } from '@/components/icons';
|
||||
import {
|
||||
IconConceptBlock,
|
||||
IconDownload,
|
||||
IconRSForm,
|
||||
IconRSFormImported,
|
||||
IconRSFormOwned,
|
||||
IconSynthesis
|
||||
} from '@/components/icons';
|
||||
import { cn } from '@/components/utils';
|
||||
import { ValueStats } from '@/components/view';
|
||||
|
||||
|
@ -11,10 +18,10 @@ interface OssStatsProps {
|
|||
|
||||
export function OssStats({ className, stats }: OssStatsProps) {
|
||||
return (
|
||||
<div className={cn('grid grid-cols-3 gap-1 justify-items-end', className)}>
|
||||
<div className={cn('grid grid-cols-4 gap-1 justify-items-end', className)}>
|
||||
<div id='count_operations' className='w-fit flex gap-3 hover:cursor-default '>
|
||||
<span>Всего</span>
|
||||
<span>{stats.count_operations}</span>
|
||||
<span>{stats.count_all}</span>
|
||||
</div>
|
||||
<ValueStats
|
||||
id='count_inputs'
|
||||
|
@ -28,6 +35,12 @@ export function OssStats({ className, stats }: OssStatsProps) {
|
|||
icon={<IconSynthesis size='1.25rem' className='text-primary' />}
|
||||
value={stats.count_synthesis}
|
||||
/>
|
||||
<ValueStats
|
||||
id='count_block'
|
||||
title='Блоки'
|
||||
icon={<IconConceptBlock size='1.25rem' className='text-primary' />}
|
||||
value={stats.count_block}
|
||||
/>
|
||||
|
||||
<ValueStats
|
||||
id='count_schemas'
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
'use client';
|
||||
|
||||
import { NodeResizeControl } from 'reactflow';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { IconResize } from '@/components/icons';
|
||||
|
||||
import { type BlockInternalNode } from '../../../../models/oss-layout';
|
||||
import { useOssEdit } from '../../oss-edit-context';
|
||||
|
||||
export function BlockNode(node: BlockInternalNode) {
|
||||
const { selected, schema } = useOssEdit();
|
||||
const singleSelected = selected.length === 1 ? selected[0] : null;
|
||||
const isParent = !singleSelected ? false : schema.hierarchy.at(singleSelected)?.inputs.includes(node.data.block.id);
|
||||
return (
|
||||
<>
|
||||
<NodeResizeControl minWidth={160} minHeight={100}>
|
||||
<IconResize size={8} className='absolute bottom-[2px] right-[2px]' />
|
||||
</NodeResizeControl>
|
||||
<div className={clsx('cc-node-block h-full w-full', isParent && 'border-primary')}>
|
||||
<div
|
||||
className={clsx(
|
||||
'w-fit mx-auto -translate-y-[14px]',
|
||||
'px-2',
|
||||
'bg-background rounded-lg',
|
||||
'text-xs line-clamp-1 text-ellipsis',
|
||||
'pointer-events-auto'
|
||||
)}
|
||||
>
|
||||
{node.data.label}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
import { Handle, Position } from 'reactflow';
|
||||
|
||||
import { type OssNodeInternal } from '../../../../models/oss-layout';
|
||||
import { type OperationInternalNode } from '../../../../models/oss-layout';
|
||||
|
||||
import { NodeCore } from './node-core';
|
||||
|
||||
export function InputNode(node: OssNodeInternal) {
|
||||
export function InputNode(node: OperationInternalNode) {
|
||||
return (
|
||||
<>
|
||||
<NodeCore node={node} />
|
||||
|
|
|
@ -7,14 +7,17 @@ import { Indicator } from '@/components/view';
|
|||
import { globalIDs } from '@/utils/constants';
|
||||
|
||||
import { OperationType } from '../../../../backend/types';
|
||||
import { type OssNodeInternal } from '../../../../models/oss-layout';
|
||||
import { type OperationInternalNode } from '../../../../models/oss-layout';
|
||||
import { useOperationTooltipStore } from '../../../../stores/operation-tooltip';
|
||||
|
||||
export const OPERATION_NODE_WIDTH = 150;
|
||||
export const OPERATION_NODE_HEIGHT = 40;
|
||||
|
||||
// characters - threshold for long labels - small font
|
||||
const LONG_LABEL_CHARS = 14;
|
||||
|
||||
interface NodeCoreProps {
|
||||
node: OssNodeInternal;
|
||||
node: OperationInternalNode;
|
||||
}
|
||||
|
||||
export function NodeCore({ node }: NodeCoreProps) {
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
import { Handle, Position } from 'reactflow';
|
||||
|
||||
import { type OssNodeInternal } from '../../../../models/oss-layout';
|
||||
import { type OperationInternalNode } from '../../../../models/oss-layout';
|
||||
|
||||
import { NodeCore } from './node-core';
|
||||
|
||||
export function OperationNode(node: OssNodeInternal) {
|
||||
export function OperationNode(node: OperationInternalNode) {
|
||||
return (
|
||||
<>
|
||||
<Handle type='target' position={Position.Top} id='left' style={{ left: 40 }} />
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import { type NodeTypes } from 'reactflow';
|
||||
|
||||
import { BlockNode } from './block-node';
|
||||
import { InputNode } from './input-node';
|
||||
import { OperationNode } from './operation-node';
|
||||
|
||||
export const OssNodeTypes: NodeTypes = {
|
||||
synthesis: OperationNode,
|
||||
input: InputNode
|
||||
input: InputNode,
|
||||
block: BlockNode
|
||||
};
|
||||
|
|
|
@ -2,12 +2,27 @@
|
|||
|
||||
/* stylelint-disable selector-class-pattern */
|
||||
|
||||
.react-flow__resize-control.handle {
|
||||
background-color: transparent;
|
||||
|
||||
z-index: var(--z-index-navigation);
|
||||
color: var(--color-muted-foreground);
|
||||
|
||||
&:hover {
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.react-flow__node-input,
|
||||
.react-flow__node-synthesis {
|
||||
cursor: pointer;
|
||||
|
||||
border-radius: 5px;
|
||||
padding: 2px;
|
||||
width: 150px;
|
||||
height: fit-content;
|
||||
|
||||
outline-offset: -2px;
|
||||
|
@ -24,3 +39,35 @@
|
|||
border-color: var(--color-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.react-flow__node-block {
|
||||
cursor: auto;
|
||||
|
||||
border-radius: 5px;
|
||||
border-width: 0;
|
||||
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
background-color: transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.cc-node-block {
|
||||
border-radius: 5px;
|
||||
border-style: dashed;
|
||||
border-width: 2px;
|
||||
|
||||
padding: 4px;
|
||||
|
||||
color: var(--color-muted-foreground);
|
||||
|
||||
.selected & {
|
||||
color: var(--color-foreground);
|
||||
border-color: var(--color-graph-selected);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,9 +8,12 @@ import {
|
|||
useEdgesState,
|
||||
useNodesState,
|
||||
useOnSelectionChange,
|
||||
useReactFlow
|
||||
useReactFlow,
|
||||
useStoreApi
|
||||
} from 'reactflow';
|
||||
|
||||
import { type IOperationSchema } from '@/features/oss/models/oss';
|
||||
|
||||
import { useMainHeight } from '@/stores/app-layout';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
|
@ -18,7 +21,7 @@ import { PARAMETER } from '@/utils/constants';
|
|||
import { useMutatingOss } from '../../../backend/use-mutating-oss';
|
||||
import { useUpdateLayout } from '../../../backend/use-update-layout';
|
||||
import { GRID_SIZE } from '../../../models/oss-api';
|
||||
import { type OssNode } from '../../../models/oss-layout';
|
||||
import { type OssNode, type Position2D } from '../../../models/oss-layout';
|
||||
import { useOperationTooltipStore } from '../../../stores/operation-tooltip';
|
||||
import { useOSSGraphStore } from '../../../stores/oss-graph';
|
||||
import { useOssEdit } from '../oss-edit-context';
|
||||
|
@ -32,6 +35,10 @@ import './graph/styles.css';
|
|||
|
||||
const ZOOM_MAX = 2;
|
||||
const ZOOM_MIN = 0.5;
|
||||
|
||||
const Z_BLOCK = 1;
|
||||
const Z_SCHEMA = 10;
|
||||
|
||||
export const VIEW_PADDING = 0.2;
|
||||
|
||||
export function OssFlow() {
|
||||
|
@ -45,6 +52,8 @@ export function OssFlow() {
|
|||
canDeleteOperation: canDelete
|
||||
} = useOssEdit();
|
||||
const { fitView, screenToFlowPosition } = useReactFlow();
|
||||
const store = useStoreApi();
|
||||
const { resetSelectedElements } = store.getState();
|
||||
|
||||
const isProcessing = useMutatingOss();
|
||||
|
||||
|
@ -79,14 +88,36 @@ export function OssFlow() {
|
|||
});
|
||||
|
||||
useEffect(() => {
|
||||
setNodes(
|
||||
schema.operations.map(operation => ({
|
||||
setNodes([
|
||||
...schema.hierarchy
|
||||
.topologicalOrder()
|
||||
.filter(id => id < 0)
|
||||
.map(id => {
|
||||
const block = schema.blockByID.get(-id)!;
|
||||
return {
|
||||
id: String(id),
|
||||
type: 'block',
|
||||
data: { label: block.title, block: block },
|
||||
position: computeRelativePosition(schema, { x: block.x, y: block.y }, block.parent),
|
||||
width: block.width,
|
||||
height: block.height,
|
||||
parentId: block.parent ? `-${block.parent}` : undefined,
|
||||
expandParent: true,
|
||||
extent: 'parent' as const,
|
||||
zIndex: Z_BLOCK
|
||||
};
|
||||
}),
|
||||
...schema.operations.map(operation => ({
|
||||
id: String(operation.id),
|
||||
type: operation.operation_type.toString(),
|
||||
data: { label: operation.alias, operation: operation },
|
||||
position: { x: operation.x, y: operation.y },
|
||||
type: operation.operation_type.toString()
|
||||
position: computeRelativePosition(schema, { x: operation.x, y: operation.y }, operation.parent),
|
||||
parentId: operation.parent ? `-${operation.parent}` : undefined,
|
||||
expandParent: true,
|
||||
extent: 'parent' as const,
|
||||
zIndex: Z_SCHEMA
|
||||
}))
|
||||
);
|
||||
]);
|
||||
setEdges(
|
||||
schema.arguments.map((argument, index) => ({
|
||||
id: String(index),
|
||||
|
@ -114,7 +145,7 @@ export function OssFlow() {
|
|||
defaultX: targetPosition.x,
|
||||
defaultY: targetPosition.y,
|
||||
layout: getLayout(),
|
||||
initialInputs: selected,
|
||||
initialInputs: selected.filter(id => id > 0),
|
||||
onCreate: () =>
|
||||
setTimeout(() => fitView({ duration: PARAMETER.zoomDuration, padding: VIEW_PADDING }), PARAMETER.refreshTimeout)
|
||||
});
|
||||
|
@ -157,7 +188,16 @@ export function OssFlow() {
|
|||
}
|
||||
|
||||
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||
if (isProcessing || !isMutable) {
|
||||
if (isProcessing) {
|
||||
return;
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
resetSelectedElements();
|
||||
return;
|
||||
}
|
||||
if (!isMutable) {
|
||||
return;
|
||||
}
|
||||
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyS') {
|
||||
|
@ -217,3 +257,20 @@ export function OssFlow() {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// -------- Internals --------
|
||||
function computeRelativePosition(schema: IOperationSchema, position: Position2D, parent: number | null): Position2D {
|
||||
if (!parent) {
|
||||
return position;
|
||||
}
|
||||
|
||||
const parentBlock = schema.blockByID.get(parent);
|
||||
if (!parentBlock) {
|
||||
return position;
|
||||
}
|
||||
|
||||
return {
|
||||
x: position.x - parentBlock.x,
|
||||
y: position.y - parentBlock.y
|
||||
};
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ export function ToolbarOssGraph({
|
|||
const { schema, selected, isMutable, canDeleteOperation: canDelete } = useOssEdit();
|
||||
const isProcessing = useMutatingOss();
|
||||
const { fitView } = useReactFlow();
|
||||
const selectedOperation = schema.operationByID.get(selected[0]);
|
||||
const selectedOperation = selected.length !== 1 ? null : schema.operationByID.get(selected[0]) ?? null;
|
||||
const getLayout = useGetLayout();
|
||||
|
||||
const showGrid = useOSSGraphStore(state => state.showGrid);
|
||||
|
@ -97,7 +97,7 @@ export function ToolbarOssGraph({
|
|||
}
|
||||
|
||||
function handleOperationExecute() {
|
||||
if (selected.length !== 1 || !readyForSynthesis || !selectedOperation) {
|
||||
if (!readyForSynthesis || !selectedOperation) {
|
||||
return;
|
||||
}
|
||||
void operationExecute({
|
||||
|
@ -106,8 +106,8 @@ export function ToolbarOssGraph({
|
|||
});
|
||||
}
|
||||
|
||||
function handleEditOperation() {
|
||||
if (selected.length !== 1 || !selectedOperation) {
|
||||
function handleEditItem() {
|
||||
if (!selectedOperation) {
|
||||
return;
|
||||
}
|
||||
showEditOperation({
|
||||
|
@ -202,7 +202,7 @@ export function ToolbarOssGraph({
|
|||
titleHtml={prepareTooltip('Редактировать выбранную', 'Двойной клик')}
|
||||
aria-label='Редактировать выбранную'
|
||||
icon={<IconEdit2 size='1.25rem' className='icon-primary' />}
|
||||
onClick={handleEditOperation}
|
||||
onClick={handleEditItem}
|
||||
disabled={selected.length !== 1 || isProcessing}
|
||||
/>
|
||||
<MiniButton
|
||||
|
|
|
@ -1,17 +1,49 @@
|
|||
import { useReactFlow } from 'reactflow';
|
||||
import { type Node, useReactFlow } from 'reactflow';
|
||||
|
||||
import { DEFAULT_BLOCK_HEIGHT, DEFAULT_BLOCK_WIDTH } from '@/features/oss/backend/oss-loader';
|
||||
import { type IOssLayout } from '@/features/oss/backend/types';
|
||||
import { type IOperationSchema } from '@/features/oss/models/oss';
|
||||
import { type Position2D } from '@/features/oss/models/oss-layout';
|
||||
|
||||
import { useOssEdit } from '../oss-edit-context';
|
||||
|
||||
export function useGetLayout() {
|
||||
const { getNodes } = useReactFlow();
|
||||
const { schema } = useOssEdit();
|
||||
|
||||
return function getLayout(): IOssLayout {
|
||||
const nodes = getNodes();
|
||||
const nodeById = new Map(nodes.map(node => [node.id, node]));
|
||||
return {
|
||||
operations: getNodes().map(node => ({
|
||||
id: Number(node.id),
|
||||
x: node.position.x,
|
||||
y: node.position.y
|
||||
})),
|
||||
blocks: []
|
||||
operations: nodes
|
||||
.filter(node => node.type !== 'block')
|
||||
.map(node => ({
|
||||
id: Number(node.id),
|
||||
...computeAbsolutePosition(node, schema, nodeById)
|
||||
})),
|
||||
blocks: nodes
|
||||
.filter(node => node.type === 'block')
|
||||
.map(node => ({
|
||||
id: -Number(node.id),
|
||||
...computeAbsolutePosition(node, schema, nodeById),
|
||||
width: node.width ?? DEFAULT_BLOCK_WIDTH,
|
||||
height: node.height ?? DEFAULT_BLOCK_HEIGHT
|
||||
}))
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// ------- Internals -------
|
||||
function computeAbsolutePosition(target: Node, schema: IOperationSchema, nodeById: Map<string, Node>): Position2D {
|
||||
const nodes = schema.hierarchy.expandAllInputs([Number(target.id)]);
|
||||
let x = target.position.x;
|
||||
let y = target.position.y;
|
||||
for (const nodeID of nodes) {
|
||||
const node = nodeById.get(String(nodeID));
|
||||
if (node) {
|
||||
x += node.position.x;
|
||||
y += node.position.y;
|
||||
}
|
||||
}
|
||||
return { x, y };
|
||||
}
|
||||
|
|
|
@ -47,27 +47,3 @@
|
|||
box-shadow: 0 0 0 2px var(--color-selected) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.react-flow__node-token,
|
||||
.react-flow__node-concept {
|
||||
/* stylelint-disable-next-line at-rule-no-deprecated */
|
||||
cursor: default;
|
||||
|
||||
border-radius: 100%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
outline-offset: -2px;
|
||||
outline-style: solid;
|
||||
outline-color: transparent;
|
||||
|
||||
transition-property: outline-offset;
|
||||
transition-timing-function: var(--transition-bezier);
|
||||
transition-duration: var(--duration-select);
|
||||
|
||||
&.selected {
|
||||
outline-offset: 4px;
|
||||
outline-color: var(--color-graph-selected);
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user