mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-25 20:40:36 +03:00
F: Refactor node ID and improve layout for new items
This commit is contained in:
parent
7059c352b6
commit
ee847043c8
|
@ -7,7 +7,15 @@ import { type ILibraryItem } from '@/features/library';
|
|||
import { Graph } from '@/models/graph';
|
||||
import { type RO } from '@/utils/meta';
|
||||
|
||||
import { type IBlock, type IOperation, type IOperationSchema, type IOperationSchemaStats } from '../models/oss';
|
||||
import {
|
||||
type IBlock,
|
||||
type IOperation,
|
||||
type IOperationSchema,
|
||||
type IOperationSchemaStats,
|
||||
type IOssItem,
|
||||
NodeType
|
||||
} from '../models/oss';
|
||||
import { constructNodeID } from '../models/oss-api';
|
||||
import { BLOCK_NODE_MIN_HEIGHT, BLOCK_NODE_MIN_WIDTH } from '../pages/oss-page/editor-oss-graph/graph/block-node';
|
||||
|
||||
import { type IOperationSchemaDTO, OperationType } from './types';
|
||||
|
@ -16,8 +24,9 @@ import { type IOperationSchemaDTO, OperationType } from './types';
|
|||
export class OssLoader {
|
||||
private oss: IOperationSchema;
|
||||
private graph: Graph = new Graph();
|
||||
private hierarchy: Graph = new Graph();
|
||||
private hierarchy: Graph<string> = new Graph<string>();
|
||||
private operationByID = new Map<number, IOperation>();
|
||||
private itemByNodeID = new Map<string, IOssItem>();
|
||||
private blockByID = new Map<number, IBlock>();
|
||||
private schemaIDs: number[] = [];
|
||||
private items: RO<ILibraryItem[]>;
|
||||
|
@ -37,6 +46,7 @@ export class OssLoader {
|
|||
|
||||
result.operationByID = this.operationByID;
|
||||
result.blockByID = this.blockByID;
|
||||
result.itemByNodeID = this.itemByNodeID;
|
||||
result.graph = this.graph;
|
||||
result.hierarchy = this.hierarchy;
|
||||
result.schemas = this.schemaIDs;
|
||||
|
@ -46,18 +56,24 @@ export class OssLoader {
|
|||
|
||||
private prepareLookups() {
|
||||
this.oss.operations.forEach(operation => {
|
||||
operation.nodeID = constructNodeID(NodeType.OPERATION, operation.id);
|
||||
operation.nodeType = NodeType.OPERATION;
|
||||
this.itemByNodeID.set(operation.nodeID, operation);
|
||||
this.operationByID.set(operation.id, operation);
|
||||
this.graph.addNode(operation.id);
|
||||
this.hierarchy.addNode(operation.id);
|
||||
this.hierarchy.addNode(operation.nodeID);
|
||||
if (operation.parent) {
|
||||
this.hierarchy.addEdge(-operation.parent, operation.id);
|
||||
this.hierarchy.addEdge(constructNodeID(NodeType.BLOCK, operation.parent), operation.nodeID);
|
||||
}
|
||||
});
|
||||
this.oss.blocks.forEach(block => {
|
||||
block.nodeID = constructNodeID(NodeType.BLOCK, block.id);
|
||||
block.nodeType = NodeType.BLOCK;
|
||||
this.itemByNodeID.set(block.nodeID, block);
|
||||
this.blockByID.set(block.id, block);
|
||||
this.hierarchy.addNode(-block.id);
|
||||
this.hierarchy.addNode(block.nodeID);
|
||||
if (block.parent) {
|
||||
this.hierarchy.addEdge(-block.parent, -block.id);
|
||||
this.hierarchy.addEdge(constructNodeID(NodeType.BLOCK, block.parent), block.nodeID);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -72,6 +72,12 @@ export type IRelocateConstituentsDTO = z.infer<typeof schemaRelocateConstituents
|
|||
/** Represents {@link IConstituenta} reference. */
|
||||
export type IConstituentaReference = z.infer<typeof schemaConstituentaReference>;
|
||||
|
||||
/** Represents {@link IOperation} position. */
|
||||
export type IOperationPosition = z.infer<typeof schemaOperationPosition>;
|
||||
|
||||
/** Represents {@link IBlock} position. */
|
||||
export type IBlockPosition = z.infer<typeof schemaBlockPosition>;
|
||||
|
||||
// ====== Schemas ======
|
||||
export const schemaOperationType = z.enum(Object.values(OperationType) as [OperationType, ...OperationType[]]);
|
||||
|
||||
|
|
|
@ -9,24 +9,22 @@ import { ComboBox } from '@/components/input/combo-box';
|
|||
import { type Styling } from '@/components/props';
|
||||
import { cn } from '@/components/utils';
|
||||
import { NoData } from '@/components/view';
|
||||
import { type RO } from '@/utils/meta';
|
||||
|
||||
import { labelOssItem } from '../labels';
|
||||
import { type IOperationSchema, type IOssItem } from '../models/oss';
|
||||
import { getItemID, isOperation } from '../models/oss-api';
|
||||
import { type IOperationSchema, type IOssItem, NodeType } from '../models/oss';
|
||||
|
||||
const SELECTION_CLEAR_TIMEOUT = 1000;
|
||||
|
||||
interface PickMultiOperationProps extends Styling {
|
||||
value: number[];
|
||||
onChange: (newValue: number[]) => void;
|
||||
interface PickContentsProps extends Styling {
|
||||
value: IOssItem[];
|
||||
onChange: (newValue: IOssItem[]) => void;
|
||||
schema: IOperationSchema;
|
||||
rows?: number;
|
||||
exclude?: number[];
|
||||
exclude?: IOssItem[];
|
||||
disallowBlocks?: boolean;
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<RO<IOssItem>>();
|
||||
const columnHelper = createColumnHelper<IOssItem>();
|
||||
|
||||
export function PickContents({
|
||||
rows,
|
||||
|
@ -37,29 +35,26 @@ export function PickContents({
|
|||
onChange,
|
||||
className,
|
||||
...restProps
|
||||
}: PickMultiOperationProps) {
|
||||
const selectedItems = value
|
||||
.map(itemID => (itemID > 0 ? schema.operationByID.get(itemID) : schema.blockByID.get(-itemID)))
|
||||
.filter(item => item !== undefined);
|
||||
const [lastSelected, setLastSelected] = useState<RO<IOssItem> | null>(null);
|
||||
const items = [
|
||||
...(disallowBlocks ? [] : schema.blocks.filter(item => !value.includes(-item.id) && !exclude?.includes(-item.id))),
|
||||
...schema.operations.filter(item => !value.includes(item.id) && !exclude?.includes(item.id))
|
||||
}: PickContentsProps) {
|
||||
const [lastSelected, setLastSelected] = useState<IOssItem | null>(null);
|
||||
const items: IOssItem[] = [
|
||||
...(disallowBlocks ? [] : schema.blocks.filter(item => !value.includes(item) && !exclude?.includes(item))),
|
||||
...schema.operations.filter(item => !value.includes(item) && !exclude?.includes(item))
|
||||
];
|
||||
|
||||
function handleDelete(target: number) {
|
||||
function handleDelete(target: IOssItem) {
|
||||
onChange(value.filter(item => item !== target));
|
||||
}
|
||||
|
||||
function handleSelect(target: RO<IOssItem> | null) {
|
||||
function handleSelect(target: IOssItem | null) {
|
||||
if (target) {
|
||||
setLastSelected(target);
|
||||
onChange([...value, getItemID(target)]);
|
||||
onChange([...value, target]);
|
||||
setTimeout(() => setLastSelected(null), SELECTION_CLEAR_TIMEOUT);
|
||||
}
|
||||
}
|
||||
|
||||
function handleMoveUp(target: number) {
|
||||
function handleMoveUp(target: IOssItem) {
|
||||
const index = value.indexOf(target);
|
||||
if (index > 0) {
|
||||
const newSelected = [...value];
|
||||
|
@ -69,7 +64,7 @@ export function PickContents({
|
|||
}
|
||||
}
|
||||
|
||||
function handleMoveDown(target: number) {
|
||||
function handleMoveDown(target: IOssItem) {
|
||||
const index = value.indexOf(target);
|
||||
if (index < value.length - 1) {
|
||||
const newSelected = [...value];
|
||||
|
@ -80,13 +75,13 @@ export function PickContents({
|
|||
}
|
||||
|
||||
const columns = [
|
||||
columnHelper.accessor(item => isOperation(item), {
|
||||
columnHelper.accessor(item => item.nodeType === NodeType.OPERATION, {
|
||||
id: 'type',
|
||||
header: 'Тип',
|
||||
size: 150,
|
||||
minSize: 150,
|
||||
maxSize: 150,
|
||||
cell: props => <div>{isOperation(props.row.original) ? 'Операция' : 'Блок'}</div>
|
||||
cell: props => <div>{props.getValue() ? 'Операция' : 'Блок'}</div>
|
||||
}),
|
||||
columnHelper.accessor('title', {
|
||||
id: 'title',
|
||||
|
@ -106,21 +101,21 @@ export function PickContents({
|
|||
noHover
|
||||
className='px-0'
|
||||
icon={<IconRemove size='1rem' className='icon-red' />}
|
||||
onClick={() => handleDelete(getItemID(props.row.original))}
|
||||
onClick={() => handleDelete(props.row.original)}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Переместить выше'
|
||||
noHover
|
||||
className='px-0'
|
||||
icon={<IconMoveUp size='1rem' className='icon-primary' />}
|
||||
onClick={() => handleMoveUp(getItemID(props.row.original))}
|
||||
onClick={() => handleMoveUp(props.row.original)}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Переместить ниже'
|
||||
noHover
|
||||
className='px-0'
|
||||
icon={<IconMoveDown size='1rem' className='icon-primary' />}
|
||||
onClick={() => handleMoveDown(getItemID(props.row.original))}
|
||||
onClick={() => handleMoveDown(props.row.original)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
@ -134,7 +129,7 @@ export function PickContents({
|
|||
items={items}
|
||||
value={lastSelected}
|
||||
placeholder='Выберите операцию или блок'
|
||||
idFunc={item => String(getItemID(item))}
|
||||
idFunc={item => item.nodeID}
|
||||
labelValueFunc={item => labelOssItem(item)}
|
||||
labelOptionFunc={item => labelOssItem(item)}
|
||||
onChange={handleSelect}
|
||||
|
@ -145,7 +140,7 @@ export function PickContents({
|
|||
rows={rows}
|
||||
contentHeight='1.3rem'
|
||||
className='cc-scroll-y text-sm select-none border-y rounded-b-md'
|
||||
data={selectedItems}
|
||||
data={value}
|
||||
columns={columns}
|
||||
headPosition='0rem'
|
||||
noDataComponent={
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { Tooltip } from '@/components/container';
|
||||
import { globalIDs } from '@/utils/constants';
|
||||
|
||||
import { type IBlock, type IOperation } from '../models/oss';
|
||||
import { isOperation } from '../models/oss-api';
|
||||
import { NodeType } from '../models/oss';
|
||||
import { useOperationTooltipStore } from '../stores/operation-tooltip';
|
||||
|
||||
import { InfoBlock } from './info-block';
|
||||
|
@ -10,7 +9,7 @@ import { InfoOperation } from './info-operation';
|
|||
|
||||
export function OperationTooltip() {
|
||||
const hoverItem = useOperationTooltipStore(state => state.hoverItem);
|
||||
const isOperationNode = isOperation(hoverItem);
|
||||
const isOperationNode = hoverItem?.nodeType === NodeType.OPERATION;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
|
@ -20,8 +19,8 @@ export function OperationTooltip() {
|
|||
className='max-w-140 dense max-h-120! overflow-y-auto!'
|
||||
hidden={!hoverItem}
|
||||
>
|
||||
{hoverItem && isOperationNode ? <InfoOperation operation={hoverItem as IOperation} /> : null}
|
||||
{hoverItem && !isOperationNode ? <InfoBlock block={hoverItem as IBlock} /> : null}
|
||||
{hoverItem && isOperationNode ? <InfoOperation operation={hoverItem} /> : null}
|
||||
{hoverItem && !isOperationNode ? <InfoBlock block={hoverItem} /> : null}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import { useDialogsStore } from '@/stores/dialogs';
|
|||
|
||||
import { type ICreateBlockDTO, schemaCreateBlock } from '../../backend/types';
|
||||
import { useCreateBlock } from '../../backend/use-create-block';
|
||||
import { type IOssItem, NodeType } from '../../models/oss';
|
||||
import { type LayoutManager } from '../../models/oss-layout-api';
|
||||
import { BLOCK_NODE_MIN_HEIGHT, BLOCK_NODE_MIN_WIDTH } from '../../pages/oss-page/editor-oss-graph/graph/block-node';
|
||||
|
||||
|
@ -20,7 +21,7 @@ import { TabBlockChildren } from './tab-block-children';
|
|||
|
||||
export interface DlgCreateBlockProps {
|
||||
manager: LayoutManager;
|
||||
initialChildren: number[];
|
||||
initialChildren: IOssItem[];
|
||||
initialParent: number | null;
|
||||
defaultX: number;
|
||||
defaultY: number;
|
||||
|
@ -52,8 +53,8 @@ export function DlgCreateBlock() {
|
|||
position_y: defaultY,
|
||||
width: BLOCK_NODE_MIN_WIDTH,
|
||||
height: BLOCK_NODE_MIN_HEIGHT,
|
||||
children_blocks: initialChildren.filter(id => id < 0).map(id => -id),
|
||||
children_operations: initialChildren.filter(id => id > 0),
|
||||
children_blocks: initialChildren.filter(item => item.nodeType === NodeType.BLOCK).map(item => item.id),
|
||||
children_operations: initialChildren.filter(item => item.nodeType === NodeType.OPERATION).map(item => item.id),
|
||||
layout: manager.layout
|
||||
},
|
||||
mode: 'onChange'
|
||||
|
@ -65,11 +66,12 @@ export function DlgCreateBlock() {
|
|||
const isValid = !!title && !manager.oss.blocks.some(block => block.title === title);
|
||||
|
||||
function onSubmit(data: ICreateBlockDTO) {
|
||||
const rectangle = manager.calculateNewBlockPosition(data);
|
||||
const rectangle = manager.newBlockPosition(data);
|
||||
data.position_x = rectangle.x;
|
||||
data.position_y = rectangle.y;
|
||||
data.width = rectangle.width;
|
||||
data.height = rectangle.height;
|
||||
data.layout = manager.layout;
|
||||
void createBlock({ itemID: manager.oss.id, data: data }).then(response => onCreate?.(response.new_block.id));
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,8 @@ import { useDialogsStore } from '@/stores/dialogs';
|
|||
|
||||
import { type ICreateBlockDTO } from '../../backend/types';
|
||||
import { SelectParent } from '../../components/select-parent';
|
||||
import { NodeType } from '../../models/oss';
|
||||
import { constructNodeID } from '../../models/oss-api';
|
||||
|
||||
import { type DlgCreateBlockProps } from './dlg-create-block';
|
||||
|
||||
|
@ -18,10 +20,8 @@ export function TabBlockCard() {
|
|||
formState: { errors }
|
||||
} = useFormContext<ICreateBlockDTO>();
|
||||
const children_blocks = useWatch({ control, name: 'children_blocks' });
|
||||
const all_children = [
|
||||
...children_blocks,
|
||||
...manager.oss.hierarchy.expandAllOutputs(children_blocks.filter(id => id < 0).map(id => -id)).map(id => -id)
|
||||
];
|
||||
const block_ids = children_blocks.map(id => constructNodeID(NodeType.BLOCK, id));
|
||||
const all_children = [...block_ids, ...manager.oss.hierarchy.expandAllOutputs(block_ids)];
|
||||
|
||||
return (
|
||||
<div className='cc-fade-in cc-column'>
|
||||
|
@ -36,7 +36,7 @@ export function TabBlockCard() {
|
|||
control={control}
|
||||
render={({ field }) => (
|
||||
<SelectParent
|
||||
items={manager.oss.blocks.filter(block => !all_children.includes(block.id))}
|
||||
items={manager.oss.blocks.filter(block => !all_children.includes(block.nodeID))}
|
||||
value={field.value ? manager.oss.blockByID.get(field.value) ?? null : null}
|
||||
placeholder='Блок содержания не выбран'
|
||||
onChange={value => field.onChange(value ? value.id : null)}
|
||||
|
|
|
@ -6,6 +6,7 @@ import { useDialogsStore } from '@/stores/dialogs';
|
|||
|
||||
import { type ICreateBlockDTO } from '../../backend/types';
|
||||
import { PickContents } from '../../components/pick-contents';
|
||||
import { type IOssItem, NodeType } from '../../models/oss';
|
||||
|
||||
import { type DlgCreateBlockProps } from './dlg-create-block';
|
||||
|
||||
|
@ -15,19 +16,31 @@ export function TabBlockChildren() {
|
|||
const parent = useWatch({ control, name: 'item_data.parent' });
|
||||
const children_blocks = useWatch({ control, name: 'children_blocks' });
|
||||
const children_operations = useWatch({ control, name: 'children_operations' });
|
||||
const exclude = parent ? [-parent, ...manager.oss.hierarchy.expandAllInputs([-parent]).filter(id => id < 0)] : [];
|
||||
|
||||
const value = [...children_blocks.map(id => -id), ...children_operations];
|
||||
const parentItem = parent ? manager.oss.blockByID.get(parent) : null;
|
||||
const internalBlocks = parentItem
|
||||
? manager.oss.hierarchy
|
||||
.expandAllInputs([parentItem.nodeID])
|
||||
.map(id => manager.oss.itemByNodeID.get(id))
|
||||
.filter(item => item !== null && item?.nodeType === NodeType.BLOCK)
|
||||
: [];
|
||||
|
||||
function handleChangeSelected(newValue: number[]) {
|
||||
const exclude = parentItem ? [parentItem, ...internalBlocks] : [];
|
||||
|
||||
const value = [
|
||||
...children_blocks.map(id => manager.oss.blockByID.get(id)!),
|
||||
...children_operations.map(id => manager.oss.operationByID.get(id)!)
|
||||
];
|
||||
|
||||
function handleChangeSelected(newValue: IOssItem[]) {
|
||||
setValue(
|
||||
'children_blocks',
|
||||
newValue.filter(id => id < 0).map(id => -id),
|
||||
newValue.filter(item => item.nodeType === NodeType.BLOCK).map(item => item.id),
|
||||
{ shouldValidate: true }
|
||||
);
|
||||
setValue(
|
||||
'children_operations',
|
||||
newValue.filter(id => id > 0),
|
||||
newValue.filter(item => item.nodeType === NodeType.OPERATION).map(item => item.id),
|
||||
{ shouldValidate: true }
|
||||
);
|
||||
}
|
||||
|
|
|
@ -64,9 +64,10 @@ export function DlgCreateOperation() {
|
|||
const isValid = !!alias && !manager.oss.operations.some(operation => operation.alias === alias);
|
||||
|
||||
function onSubmit(data: ICreateOperationDTO) {
|
||||
const target = manager.calculateNewOperationPosition(data);
|
||||
const target = manager.newOperationPosition(data);
|
||||
data.position_x = target.x;
|
||||
data.position_y = target.y;
|
||||
data.layout = manager.layout;
|
||||
void createOperation({ itemID: manager.oss.id, data: data }).then(response =>
|
||||
onCreate?.(response.new_operation.id)
|
||||
);
|
||||
|
|
|
@ -44,6 +44,7 @@ export function DlgEditBlock() {
|
|||
function onSubmit(data: IUpdateBlockDTO) {
|
||||
if (data.item_data.parent !== target.parent) {
|
||||
manager.onBlockChangeParent(data.target, data.item_data.parent);
|
||||
data.layout = manager.layout;
|
||||
}
|
||||
return updateBlock({ itemID: manager.oss.id, data });
|
||||
}
|
||||
|
|
|
@ -60,6 +60,7 @@ export function DlgEditOperation() {
|
|||
function onSubmit(data: IUpdateOperationDTO) {
|
||||
if (data.item_data.parent !== target.parent) {
|
||||
manager.onOperationChangeParent(data.target, data.item_data.parent);
|
||||
data.layout = manager.layout;
|
||||
}
|
||||
return updateOperation({ itemID: manager.oss.id, data });
|
||||
}
|
||||
|
|
|
@ -5,9 +5,9 @@ import {
|
|||
type IOperation,
|
||||
type IOssItem,
|
||||
type ISubstitutionErrorDescription,
|
||||
NodeType,
|
||||
SubstitutionErrorType
|
||||
} from './models/oss';
|
||||
import { isOperation } from './models/oss-api';
|
||||
|
||||
/** Retrieves label for {@link OperationType}. */
|
||||
export function labelOperationType(itemType: OperationType): string {
|
||||
|
@ -58,7 +58,7 @@ export function describeSubstitutionError(error: RO<ISubstitutionErrorDescriptio
|
|||
|
||||
/** Retrieves label for {@link IOssItem}. */
|
||||
export function labelOssItem(item: RO<IOssItem>): string {
|
||||
if (isOperation(item)) {
|
||||
if (item.nodeType === NodeType.OPERATION) {
|
||||
return `${(item as IOperation).alias}: ${item.title}`;
|
||||
} else {
|
||||
return `Блок: ${item.title}`;
|
||||
|
|
|
@ -20,23 +20,16 @@ import {
|
|||
} from '@/features/rsform/models/rslang-api';
|
||||
|
||||
import { infoMsg } from '@/utils/labels';
|
||||
import { type RO } from '@/utils/meta';
|
||||
|
||||
import { Graph } from '../../../models/graph';
|
||||
import { describeSubstitutionError } from '../labels';
|
||||
|
||||
import { type IOperationSchema, type IOssItem, SubstitutionErrorType } from './oss';
|
||||
import { type IOperationSchema, NodeType, SubstitutionErrorType } from './oss';
|
||||
|
||||
const STARTING_SUB_INDEX = 900; // max semantic index for starting substitution
|
||||
|
||||
/** Checks if element is {@link IOperation} or {@link IBlock}. */
|
||||
export function isOperation(item: RO<IOssItem> | null): boolean {
|
||||
return !!item && 'arguments' in item;
|
||||
}
|
||||
|
||||
/** Extract contiguous ID of {@link IOperation} or {@link IBlock}. */
|
||||
export function getItemID(item: RO<IOssItem>): number {
|
||||
return isOperation(item) ? item.id : -item.id;
|
||||
export function constructNodeID(type: NodeType, itemID: number): string {
|
||||
return type === NodeType.OPERATION ? 'o' + String(itemID) : 'b' + String(itemID);
|
||||
}
|
||||
|
||||
/** Sorts library items relevant for the specified {@link IOperationSchema}. */
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
import { type ICreateBlockDTO, type ICreateOperationDTO, type IOssLayout } from '../backend/types';
|
||||
import {
|
||||
type IBlockPosition,
|
||||
type ICreateBlockDTO,
|
||||
type ICreateOperationDTO,
|
||||
type IOperationPosition,
|
||||
type IOssLayout
|
||||
} from '../backend/types';
|
||||
|
||||
import { type IOperationSchema } from './oss';
|
||||
import { type Position2D, type Rectangle2D } from './oss-layout';
|
||||
|
@ -26,94 +32,105 @@ export class LayoutManager {
|
|||
}
|
||||
|
||||
/** Calculate insert position for a new {@link IOperation} */
|
||||
calculateNewOperationPosition(data: ICreateOperationDTO): Position2D {
|
||||
// TODO: check parent node
|
||||
|
||||
const result = { x: data.position_x, y: data.position_y };
|
||||
newOperationPosition(data: ICreateOperationDTO): Position2D {
|
||||
let result = { x: data.position_x, y: data.position_y };
|
||||
const operations = this.layout.operations;
|
||||
const parentNode = this.layout.blocks.find(pos => pos.id === data.item_data.parent);
|
||||
if (operations.length === 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (data.arguments.length === 0) {
|
||||
let inputsPositions = operations.filter(pos =>
|
||||
this.oss.operations.find(operation => operation.arguments.length === 0 && operation.id === pos.id)
|
||||
);
|
||||
if (inputsPositions.length === 0) {
|
||||
inputsPositions = operations;
|
||||
}
|
||||
const maxX = Math.max(...inputsPositions.map(node => node.x));
|
||||
const minY = Math.min(...inputsPositions.map(node => node.y));
|
||||
result.x = maxX + DISTANCE_X;
|
||||
result.y = minY;
|
||||
if (data.arguments.length !== 0) {
|
||||
result = calculatePositionFromArgs(data.arguments, operations);
|
||||
} else if (parentNode) {
|
||||
result.x = parentNode.x + MIN_DISTANCE;
|
||||
result.y = parentNode.y + MIN_DISTANCE;
|
||||
} else {
|
||||
const argNodes = operations.filter(pos => data.arguments.includes(pos.id));
|
||||
const maxY = Math.max(...argNodes.map(node => node.y));
|
||||
const minX = Math.min(...argNodes.map(node => node.x));
|
||||
const maxX = Math.max(...argNodes.map(node => node.x));
|
||||
result.x = Math.ceil((maxX + minX) / 2 / GRID_SIZE) * GRID_SIZE;
|
||||
result.y = maxY + DISTANCE_Y;
|
||||
result = this.calculatePositionForFreeOperation(result);
|
||||
}
|
||||
|
||||
let flagIntersect = false;
|
||||
do {
|
||||
flagIntersect = operations.some(
|
||||
position => Math.abs(position.x - result.x) < MIN_DISTANCE && Math.abs(position.y - result.y) < MIN_DISTANCE
|
||||
);
|
||||
if (flagIntersect) {
|
||||
result.x += MIN_DISTANCE;
|
||||
result.y += MIN_DISTANCE;
|
||||
result = preventOverlap(
|
||||
{ ...result, width: OPERATION_NODE_WIDTH, height: OPERATION_NODE_HEIGHT },
|
||||
operations.map(node => ({ ...node, width: OPERATION_NODE_WIDTH, height: OPERATION_NODE_HEIGHT }))
|
||||
);
|
||||
|
||||
if (parentNode) {
|
||||
const borderX = result.x + OPERATION_NODE_WIDTH + MIN_DISTANCE;
|
||||
const borderY = result.y + OPERATION_NODE_HEIGHT + MIN_DISTANCE;
|
||||
if (borderX > parentNode.x + parentNode.width) {
|
||||
parentNode.width = borderX - parentNode.x;
|
||||
}
|
||||
} while (flagIntersect);
|
||||
return result;
|
||||
if (borderY > parentNode.y + parentNode.height) {
|
||||
parentNode.height = borderY - parentNode.y;
|
||||
}
|
||||
// TODO: trigger cascading updates
|
||||
}
|
||||
|
||||
return { x: result.x, y: result.y };
|
||||
}
|
||||
|
||||
/** Calculate insert position for a new {@link IBlock} */
|
||||
calculateNewBlockPosition(data: ICreateBlockDTO): Rectangle2D {
|
||||
newBlockPosition(data: ICreateBlockDTO): Rectangle2D {
|
||||
const block_nodes = data.children_blocks
|
||||
.map(id => this.layout.blocks.find(block => block.id === id))
|
||||
.filter(node => !!node);
|
||||
const operation_nodes = data.children_operations
|
||||
.map(id => this.layout.operations.find(operation => operation.id === id))
|
||||
.filter(node => !!node);
|
||||
const parentNode = this.layout.blocks.find(pos => pos.id === data.item_data.parent);
|
||||
|
||||
let result: Rectangle2D = { x: data.position_x, y: data.position_y, width: data.width, height: data.height };
|
||||
|
||||
if (block_nodes.length !== 0 || operation_nodes.length !== 0) {
|
||||
result = calculatePositionFromChildren(
|
||||
{ x: data.position_x, y: data.position_y, width: data.width, height: data.height },
|
||||
operation_nodes,
|
||||
block_nodes
|
||||
);
|
||||
} else if (parentNode) {
|
||||
result = {
|
||||
x: parentNode.x + MIN_DISTANCE,
|
||||
y: parentNode.y + MIN_DISTANCE,
|
||||
width: data.width,
|
||||
height: data.height
|
||||
};
|
||||
} else {
|
||||
result = this.calculatePositionForFreeBlock(result);
|
||||
}
|
||||
|
||||
if (block_nodes.length === 0 && operation_nodes.length === 0) {
|
||||
return { x: data.position_x, y: data.position_y, width: data.width, height: data.height };
|
||||
if (parentNode) {
|
||||
const siblings = this.oss.blocks.filter(block => block.parent === parentNode.id).map(block => block.id);
|
||||
if (siblings.length > 0) {
|
||||
result = preventOverlap(
|
||||
result,
|
||||
this.layout.blocks.filter(block => siblings.includes(block.id))
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const rootBlocks = this.oss.blocks.filter(block => block.parent === null).map(block => block.id);
|
||||
if (rootBlocks.length > 0) {
|
||||
result = preventOverlap(
|
||||
result,
|
||||
this.layout.blocks.filter(block => rootBlocks.includes(block.id))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let left = undefined;
|
||||
let top = undefined;
|
||||
let right = undefined;
|
||||
let bottom = undefined;
|
||||
|
||||
for (const block of block_nodes) {
|
||||
left = !left ? block.x - MIN_DISTANCE : Math.min(left, block.x - MIN_DISTANCE);
|
||||
top = !top ? block.y - MIN_DISTANCE : Math.min(top, block.y - MIN_DISTANCE);
|
||||
right = !right
|
||||
? Math.max(left + data.width, block.x + block.width + MIN_DISTANCE)
|
||||
: Math.max(right, block.x + block.width + MIN_DISTANCE);
|
||||
bottom = !bottom
|
||||
? Math.max(top + data.height, block.y + block.height + MIN_DISTANCE)
|
||||
: Math.max(bottom, block.y + block.height + MIN_DISTANCE);
|
||||
if (parentNode) {
|
||||
const borderX = result.x + result.width + MIN_DISTANCE;
|
||||
const borderY = result.y + result.height + MIN_DISTANCE;
|
||||
if (borderX > parentNode.x + parentNode.width) {
|
||||
parentNode.width = borderX - parentNode.x;
|
||||
}
|
||||
if (borderY > parentNode.y + parentNode.height) {
|
||||
parentNode.height = borderY - parentNode.y;
|
||||
}
|
||||
// TODO: trigger cascading updates
|
||||
}
|
||||
|
||||
for (const operation of operation_nodes) {
|
||||
left = !left ? operation.x - MIN_DISTANCE : Math.min(left, operation.x - MIN_DISTANCE);
|
||||
top = !top ? operation.y - MIN_DISTANCE : Math.min(top, operation.y - MIN_DISTANCE);
|
||||
right = !right
|
||||
? Math.max(left + data.width, operation.x + OPERATION_NODE_WIDTH + MIN_DISTANCE)
|
||||
: Math.max(right, operation.x + OPERATION_NODE_WIDTH + MIN_DISTANCE);
|
||||
bottom = !bottom
|
||||
? Math.max(top + data.height, operation.y + OPERATION_NODE_HEIGHT + MIN_DISTANCE)
|
||||
: Math.max(bottom, operation.y + OPERATION_NODE_HEIGHT + MIN_DISTANCE);
|
||||
}
|
||||
|
||||
return {
|
||||
x: left ?? data.position_x,
|
||||
y: top ?? data.position_y,
|
||||
width: right && left ? right - left : data.width,
|
||||
height: bottom && top ? bottom - top : data.height
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Update layout when parent changes */
|
||||
|
@ -125,4 +142,126 @@ export class LayoutManager {
|
|||
onBlockChangeParent(targetID: number, newParent: number | null) {
|
||||
console.error('not implemented', targetID, newParent);
|
||||
}
|
||||
|
||||
private calculatePositionForFreeOperation(initial: Position2D): Position2D {
|
||||
const operations = this.layout.operations;
|
||||
if (operations.length === 0) {
|
||||
return initial;
|
||||
}
|
||||
|
||||
const freeInputs = this.oss.operations
|
||||
.filter(operation => operation.arguments.length === 0 && operation.parent === null)
|
||||
.map(operation => operation.id);
|
||||
let inputsPositions = operations.filter(pos => freeInputs.includes(pos.id));
|
||||
if (inputsPositions.length === 0) {
|
||||
inputsPositions = operations;
|
||||
}
|
||||
const maxX = Math.max(...inputsPositions.map(node => node.x));
|
||||
const minY = Math.min(...inputsPositions.map(node => node.y));
|
||||
return {
|
||||
x: maxX + DISTANCE_X,
|
||||
y: minY
|
||||
};
|
||||
}
|
||||
|
||||
private calculatePositionForFreeBlock(initial: Rectangle2D): Rectangle2D {
|
||||
const rootBlocks = this.oss.blocks.filter(block => block.parent === null).map(block => block.id);
|
||||
const blocksPositions = this.layout.blocks.filter(pos => rootBlocks.includes(pos.id));
|
||||
if (blocksPositions.length === 0) {
|
||||
return initial;
|
||||
}
|
||||
const maxX = Math.max(...blocksPositions.map(node => node.x + node.width));
|
||||
const minY = Math.min(...blocksPositions.map(node => node.y));
|
||||
return { ...initial, x: maxX + MIN_DISTANCE, y: minY };
|
||||
}
|
||||
}
|
||||
|
||||
// ======= Internals =======
|
||||
function rectanglesOverlap(a: Rectangle2D, b: Rectangle2D): boolean {
|
||||
return !(
|
||||
a.x + a.width + MIN_DISTANCE <= b.x ||
|
||||
b.x + b.width + MIN_DISTANCE <= a.x ||
|
||||
a.y + a.height + MIN_DISTANCE <= b.y ||
|
||||
b.y + b.height + MIN_DISTANCE <= a.y
|
||||
);
|
||||
}
|
||||
|
||||
function getOverlapAmount(a: Rectangle2D, b: Rectangle2D): Position2D {
|
||||
const xOverlap = Math.max(0, Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x));
|
||||
const yOverlap = Math.max(0, Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y));
|
||||
return { x: xOverlap, y: yOverlap };
|
||||
}
|
||||
|
||||
function preventOverlap(target: Rectangle2D, fixedRectangles: Rectangle2D[]): Rectangle2D {
|
||||
let hasOverlap: boolean;
|
||||
do {
|
||||
hasOverlap = false;
|
||||
for (const fixed of fixedRectangles) {
|
||||
if (rectanglesOverlap(target, fixed)) {
|
||||
hasOverlap = true;
|
||||
const overlap = getOverlapAmount(target, fixed);
|
||||
if (overlap.x >= overlap.y) {
|
||||
target.x += overlap.x + MIN_DISTANCE;
|
||||
} else {
|
||||
target.y += overlap.y + MIN_DISTANCE;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} while (hasOverlap);
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
function calculatePositionFromArgs(args: number[], operations: IOperationPosition[]): Position2D {
|
||||
const argNodes = operations.filter(pos => args.includes(pos.id));
|
||||
const maxY = Math.max(...argNodes.map(node => node.y));
|
||||
const minX = Math.min(...argNodes.map(node => node.x));
|
||||
const maxX = Math.max(...argNodes.map(node => node.x));
|
||||
return {
|
||||
x: Math.ceil((maxX + minX) / 2 / GRID_SIZE) * GRID_SIZE,
|
||||
y: maxY + DISTANCE_Y
|
||||
};
|
||||
}
|
||||
|
||||
function calculatePositionFromChildren(
|
||||
initial: Rectangle2D,
|
||||
operations: IOperationPosition[],
|
||||
blocks: IBlockPosition[]
|
||||
): Rectangle2D {
|
||||
let left = undefined;
|
||||
let top = undefined;
|
||||
let right = undefined;
|
||||
let bottom = undefined;
|
||||
|
||||
for (const block of blocks) {
|
||||
left = left === undefined ? block.x - MIN_DISTANCE : Math.min(left, block.x - MIN_DISTANCE);
|
||||
top = top === undefined ? block.y - MIN_DISTANCE : Math.min(top, block.y - MIN_DISTANCE);
|
||||
right =
|
||||
right === undefined
|
||||
? Math.max(left + initial.width, block.x + block.width + MIN_DISTANCE)
|
||||
: Math.max(right, block.x + block.width + MIN_DISTANCE);
|
||||
bottom = !bottom
|
||||
? Math.max(top + initial.height, block.y + block.height + MIN_DISTANCE)
|
||||
: Math.max(bottom, block.y + block.height + MIN_DISTANCE);
|
||||
}
|
||||
|
||||
for (const operation of operations) {
|
||||
left = left === undefined ? operation.x - MIN_DISTANCE : Math.min(left, operation.x - MIN_DISTANCE);
|
||||
top = top === undefined ? operation.y - MIN_DISTANCE : Math.min(top, operation.y - MIN_DISTANCE);
|
||||
right =
|
||||
right === undefined
|
||||
? Math.max(left + initial.width, operation.x + OPERATION_NODE_WIDTH + MIN_DISTANCE)
|
||||
: Math.max(right, operation.x + OPERATION_NODE_WIDTH + MIN_DISTANCE);
|
||||
bottom = !bottom
|
||||
? Math.max(top + initial.height, operation.y + OPERATION_NODE_HEIGHT + MIN_DISTANCE)
|
||||
: Math.max(bottom, operation.y + OPERATION_NODE_HEIGHT + MIN_DISTANCE);
|
||||
}
|
||||
|
||||
return {
|
||||
x: left ?? initial.x,
|
||||
y: top ?? initial.y,
|
||||
width: right !== undefined && left !== undefined ? right - left : initial.width,
|
||||
height: bottom !== undefined && top !== undefined ? bottom - top : initial.height
|
||||
};
|
||||
}
|
||||
|
|
|
@ -11,8 +11,17 @@ import {
|
|||
type IOperationSchemaDTO
|
||||
} from '../backend/types';
|
||||
|
||||
/** Represents OSS node type. */
|
||||
export const NodeType = {
|
||||
OPERATION: 1,
|
||||
BLOCK: 2
|
||||
} as const;
|
||||
export type NodeType = (typeof NodeType)[keyof typeof NodeType];
|
||||
|
||||
/** Represents Operation. */
|
||||
export interface IOperation extends IOperationDTO {
|
||||
nodeID: string;
|
||||
nodeType: typeof NodeType.OPERATION;
|
||||
x: number;
|
||||
y: number;
|
||||
is_owned: boolean;
|
||||
|
@ -23,12 +32,17 @@ export interface IOperation extends IOperationDTO {
|
|||
|
||||
/** Represents Block. */
|
||||
export interface IBlock extends IBlockDTO {
|
||||
nodeID: string;
|
||||
nodeType: typeof NodeType.BLOCK;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
/** Represents item of OperationSchema. */
|
||||
export type IOssItem = IOperation | IBlock;
|
||||
|
||||
/** Represents {@link IOperationSchema} statistics. */
|
||||
export interface IOperationSchemaStats {
|
||||
count_all: number;
|
||||
|
@ -45,16 +59,14 @@ export interface IOperationSchema extends IOperationSchemaDTO {
|
|||
blocks: IBlock[];
|
||||
|
||||
graph: Graph;
|
||||
hierarchy: Graph;
|
||||
hierarchy: Graph<string>;
|
||||
schemas: number[];
|
||||
stats: IOperationSchemaStats;
|
||||
operationByID: Map<number, IOperation>;
|
||||
blockByID: Map<number, IBlock>;
|
||||
itemByNodeID: Map<string, IOssItem>;
|
||||
}
|
||||
|
||||
/** Represents item of OperationSchema. */
|
||||
export type IOssItem = IOperation | IBlock;
|
||||
|
||||
/** Represents substitution error description. */
|
||||
export interface ISubstitutionErrorDescription {
|
||||
errorType: SubstitutionErrorType;
|
||||
|
|
|
@ -4,8 +4,7 @@ import { useRef } from 'react';
|
|||
|
||||
import { Dropdown } from '@/components/dropdown';
|
||||
|
||||
import { type IBlock, type IOperation, type IOssItem } from '../../../../models/oss';
|
||||
import { isOperation } from '../../../../models/oss-api';
|
||||
import { type IOssItem, NodeType } from '../../../../models/oss';
|
||||
|
||||
import { MenuBlock } from './menu-block';
|
||||
import { MenuOperation } from './menu-operation';
|
||||
|
@ -27,7 +26,6 @@ interface ContextMenuProps extends ContextMenuData {
|
|||
|
||||
export function ContextMenu({ isOpen, item, cursorX, cursorY, onHide }: ContextMenuProps) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const isOperationNode = isOperation(item);
|
||||
|
||||
function handleBlur(event: React.FocusEvent<HTMLDivElement>) {
|
||||
if (!ref.current?.contains(event.relatedTarget as Node)) {
|
||||
|
@ -49,10 +47,10 @@ export function ContextMenu({ isOpen, item, cursorX, cursorY, onHide }: ContextM
|
|||
margin={cursorY >= window.innerHeight - MENU_HEIGHT ? 'mb-3' : 'mt-3'}
|
||||
>
|
||||
{!!item ? (
|
||||
isOperationNode ? (
|
||||
<MenuOperation operation={item as IOperation} onHide={onHide} />
|
||||
item.nodeType === NodeType.OPERATION ? (
|
||||
<MenuOperation operation={item} onHide={onHide} />
|
||||
) : (
|
||||
<MenuBlock block={item as IBlock} onHide={onHide} />
|
||||
<MenuBlock block={item} onHide={onHide} />
|
||||
)
|
||||
) : null}
|
||||
</Dropdown>
|
||||
|
|
|
@ -16,15 +16,15 @@ export const BLOCK_NODE_MIN_WIDTH = 160;
|
|||
export const BLOCK_NODE_MIN_HEIGHT = 100;
|
||||
|
||||
export function BlockNode(node: BlockInternalNode) {
|
||||
const { selected, schema } = useOssEdit();
|
||||
const { selectedItems, schema } = useOssEdit();
|
||||
const dropTarget = useDraggingStore(state => state.dropTarget);
|
||||
const isDragging = useDraggingStore(state => state.isDragging);
|
||||
const showCoordinates = useOSSGraphStore(state => state.showCoordinates);
|
||||
const setHover = useOperationTooltipStore(state => state.setHoverItem);
|
||||
|
||||
const focus = selected.length === 1 ? selected[0] : null;
|
||||
const isParent = (!!focus && schema.hierarchy.at(focus)?.inputs.includes(-node.data.block.id)) ?? false;
|
||||
const isChild = (!!focus && schema.hierarchy.at(focus)?.outputs.includes(-node.data.block.id)) ?? false;
|
||||
const focus = selectedItems.length === 1 ? selectedItems[0] : null;
|
||||
const isParent = (!!focus && schema.hierarchy.at(focus.nodeID)?.inputs.includes(node.data.block.nodeID)) ?? false;
|
||||
const isChild = (!!focus && schema.hierarchy.at(focus.nodeID)?.outputs.includes(node.data.block.nodeID)) ?? false;
|
||||
return (
|
||||
<>
|
||||
<NodeResizeControl minWidth={BLOCK_NODE_MIN_WIDTH} minHeight={BLOCK_NODE_MIN_HEIGHT}>
|
||||
|
|
|
@ -21,9 +21,9 @@ interface NodeCoreProps {
|
|||
}
|
||||
|
||||
export function NodeCore({ node }: NodeCoreProps) {
|
||||
const { selected, schema } = useOssEdit();
|
||||
const focus = selected.length === 1 ? selected[0] : null;
|
||||
const isChild = (!!focus && schema.hierarchy.at(focus)?.outputs.includes(node.data.operation.id)) ?? false;
|
||||
const { selectedItems, schema } = useOssEdit();
|
||||
const focus = selectedItems.length === 1 ? selectedItems[0] : null;
|
||||
const isChild = (!!focus && schema.hierarchy.at(focus.nodeID)?.outputs.includes(node.data.operation.nodeID)) ?? false;
|
||||
|
||||
const setHover = useOperationTooltipStore(state => state.setHoverItem);
|
||||
const showCoordinates = useOSSGraphStore(state => state.showCoordinates);
|
||||
|
|
|
@ -3,12 +3,12 @@
|
|||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { type Edge, type Node, useEdgesState, useNodesState, useOnSelectionChange, useReactFlow } from 'reactflow';
|
||||
|
||||
import { type IOperationSchema } from '@/features/oss/models/oss';
|
||||
import { type Position2D } from '@/features/oss/models/oss-layout';
|
||||
import { useOSSGraphStore } from '@/features/oss/stores/oss-graph';
|
||||
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
|
||||
import { type IOperationSchema, NodeType } from '../../../models/oss';
|
||||
import { constructNodeID } from '../../../models/oss-api';
|
||||
import { type Position2D } from '../../../models/oss-layout';
|
||||
import { useOSSGraphStore } from '../../../stores/oss-graph';
|
||||
import { useOssEdit } from '../oss-edit-context';
|
||||
|
||||
import { flowOptions } from './oss-flow';
|
||||
|
@ -29,10 +29,10 @@ export const OssFlowState = ({ children }: React.PropsWithChildren) => {
|
|||
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
|
||||
|
||||
function onSelectionChange({ nodes }: { nodes: Node[] }) {
|
||||
const ids = nodes.map(node => Number(node.id));
|
||||
const ids = nodes.map(node => node.id);
|
||||
setSelected(prev => [
|
||||
...prev.filter(nodeID => ids.includes(nodeID)),
|
||||
...ids.filter(nodeID => !prev.includes(Number(nodeID)))
|
||||
...ids.filter(nodeID => !prev.includes(nodeID))
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -41,46 +41,45 @@ export const OssFlowState = ({ children }: React.PropsWithChildren) => {
|
|||
});
|
||||
|
||||
const resetGraph = useCallback(() => {
|
||||
const newNodes: Node[] = [
|
||||
...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),
|
||||
style: {
|
||||
width: block.width,
|
||||
height: block.height
|
||||
},
|
||||
parentId: block.parent ? `-${block.parent}` : undefined,
|
||||
zIndex: Z_BLOCK
|
||||
};
|
||||
}),
|
||||
...schema.operations.map(operation => ({
|
||||
id: String(operation.id),
|
||||
type: operation.operation_type.toString(),
|
||||
data: { label: operation.alias, operation: operation },
|
||||
position: computeRelativePosition(schema, { x: operation.x, y: operation.y }, operation.parent),
|
||||
parentId: operation.parent ? `-${operation.parent}` : undefined,
|
||||
zIndex: Z_SCHEMA
|
||||
}))
|
||||
];
|
||||
const newNodes: Node[] = schema.hierarchy.topologicalOrder().map(nodeID => {
|
||||
const item = schema.itemByNodeID.get(nodeID)!;
|
||||
if (item.nodeType === NodeType.BLOCK) {
|
||||
return {
|
||||
id: nodeID,
|
||||
type: 'block',
|
||||
data: { label: item.title, block: item },
|
||||
position: computeRelativePosition(schema, { x: item.x, y: item.y }, item.parent),
|
||||
style: {
|
||||
width: item.width,
|
||||
height: item.height
|
||||
},
|
||||
parentId: item.parent ? constructNodeID(NodeType.BLOCK, item.parent) : undefined,
|
||||
zIndex: Z_BLOCK
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
id: item.nodeID,
|
||||
type: item.operation_type.toString(),
|
||||
data: { label: item.alias, operation: item },
|
||||
position: computeRelativePosition(schema, { x: item.x, y: item.y }, item.parent),
|
||||
parentId: item.parent ? constructNodeID(NodeType.BLOCK, item.parent) : undefined,
|
||||
zIndex: Z_SCHEMA
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const newEdges: Edge[] = schema.arguments.map((argument, index) => ({
|
||||
id: String(index),
|
||||
source: String(argument.argument),
|
||||
target: String(argument.operation),
|
||||
type: edgeStraight ? 'straight' : 'simplebezier',
|
||||
animated: edgeAnimate,
|
||||
targetHandle:
|
||||
schema.operationByID.get(argument.argument)!.x > schema.operationByID.get(argument.operation)!.x
|
||||
? 'right'
|
||||
: 'left'
|
||||
}));
|
||||
const newEdges: Edge[] = schema.arguments.map((argument, index) => {
|
||||
const source = schema.operationByID.get(argument.argument)!;
|
||||
const target = schema.operationByID.get(argument.operation)!;
|
||||
return {
|
||||
id: String(index),
|
||||
source: source.nodeID,
|
||||
target: target.nodeID,
|
||||
type: edgeStraight ? 'straight' : 'simplebezier',
|
||||
animated: edgeAnimate,
|
||||
targetHandle: source.x > target.x ? 'right' : 'left'
|
||||
};
|
||||
});
|
||||
|
||||
setNodes(newNodes);
|
||||
setEdges(newEdges);
|
||||
|
|
|
@ -12,6 +12,7 @@ import { promptText } from '@/utils/labels';
|
|||
import { useDeleteBlock } from '../../../backend/use-delete-block';
|
||||
import { useMutatingOss } from '../../../backend/use-mutating-oss';
|
||||
import { useUpdateLayout } from '../../../backend/use-update-layout';
|
||||
import { type IOssItem, NodeType } from '../../../models/oss';
|
||||
import { type OssNode, type Position2D } from '../../../models/oss-layout';
|
||||
import { GRID_SIZE, LayoutManager } from '../../../models/oss-layout-api';
|
||||
import { useOSSGraphStore } from '../../../stores/oss-graph';
|
||||
|
@ -41,7 +42,7 @@ export const flowOptions = {
|
|||
|
||||
export function OssFlow() {
|
||||
const mainHeight = useMainHeight();
|
||||
const { navigateOperationSchema, schema, selected, isMutable, canDeleteOperation } = useOssEdit();
|
||||
const { navigateOperationSchema, schema, selected, selectedItems, isMutable, canDeleteOperation } = useOssEdit();
|
||||
const { screenToFlowPosition } = useReactFlow();
|
||||
const { containMovement, nodes, onNodesChange, edges, onEdgesChange, resetGraph, resetView } = useOssFlow();
|
||||
const store = useStoreApi();
|
||||
|
@ -76,20 +77,21 @@ export function OssFlow() {
|
|||
manager: new LayoutManager(schema, getLayout()),
|
||||
defaultX: targetPosition.x,
|
||||
defaultY: targetPosition.y,
|
||||
initialInputs: selected.filter(id => id > 0),
|
||||
initialParent: extractSingleBlock(selected),
|
||||
initialInputs: selectedItems.filter(item => item?.nodeType === NodeType.OPERATION).map(item => item.id),
|
||||
initialParent: extractBlockParent(selectedItems),
|
||||
onCreate: resetView
|
||||
});
|
||||
}
|
||||
|
||||
function handleCreateBlock() {
|
||||
const targetPosition = screenToFlowPosition({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
|
||||
const parent = extractSingleBlock(selected);
|
||||
const parent = extractBlockParent(selectedItems);
|
||||
showCreateBlock({
|
||||
manager: new LayoutManager(schema, getLayout()),
|
||||
defaultX: targetPosition.x,
|
||||
defaultY: targetPosition.y,
|
||||
initialChildren: parent !== null ? [] : selected,
|
||||
initialChildren:
|
||||
parent !== null && selectedItems.length === 1 && parent === selectedItems[0].id ? [] : selectedItems,
|
||||
initialParent: parent,
|
||||
onCreate: resetView
|
||||
});
|
||||
|
@ -99,25 +101,24 @@ export function OssFlow() {
|
|||
if (selected.length !== 1) {
|
||||
return;
|
||||
}
|
||||
if (selected[0] > 0) {
|
||||
const operation = schema.operationByID.get(selected[0]);
|
||||
if (!operation || !canDeleteOperation(operation)) {
|
||||
const item = schema.itemByNodeID.get(selected[0]);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
if (item.nodeType === NodeType.OPERATION) {
|
||||
if (!canDeleteOperation(item)) {
|
||||
return;
|
||||
}
|
||||
showDeleteOperation({
|
||||
oss: schema,
|
||||
target: operation,
|
||||
target: item,
|
||||
layout: getLayout()
|
||||
});
|
||||
} else {
|
||||
const block = schema.blockByID.get(-selected[0]);
|
||||
if (!block) {
|
||||
return;
|
||||
}
|
||||
if (!window.confirm(promptText.deleteBlock)) {
|
||||
return;
|
||||
}
|
||||
void deleteBlock({ itemID: schema.id, data: { target: block.id, layout: getLayout() } });
|
||||
void deleteBlock({ itemID: schema.id, data: { target: item.id, layout: getLayout() } });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -219,7 +220,10 @@ export function OssFlow() {
|
|||
}
|
||||
|
||||
// -------- Internals --------
|
||||
function extractSingleBlock(selected: number[]): number | null {
|
||||
const blocks = selected.filter(id => id < 0);
|
||||
return blocks.length === 1 ? -blocks[0] : null;
|
||||
function extractBlockParent(selectedItems: IOssItem[]): number | null {
|
||||
if (selectedItems.length === 1 && selectedItems[0].nodeType === NodeType.BLOCK) {
|
||||
return selectedItems[0].id;
|
||||
}
|
||||
const parents = selectedItems.map(item => item.parent).filter(id => id !== null);
|
||||
return parents.length === 0 ? null : parents[0];
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ import { OperationType } from '../../../backend/types';
|
|||
import { useExecuteOperation } from '../../../backend/use-execute-operation';
|
||||
import { useMutatingOss } from '../../../backend/use-mutating-oss';
|
||||
import { useUpdateLayout } from '../../../backend/use-update-layout';
|
||||
import { NodeType } from '../../../models/oss';
|
||||
import { LayoutManager } from '../../../models/oss-layout-api';
|
||||
import { useOssEdit } from '../oss-edit-context';
|
||||
|
||||
|
@ -48,11 +49,13 @@ export function ToolbarOssGraph({
|
|||
className,
|
||||
...restProps
|
||||
}: ToolbarOssGraphProps) {
|
||||
const { schema, selected, isMutable, canDeleteOperation: canDelete } = useOssEdit();
|
||||
const { schema, selectedItems, isMutable, canDeleteOperation: canDelete } = useOssEdit();
|
||||
const isProcessing = useMutatingOss();
|
||||
const { resetView } = useOssFlow();
|
||||
const selectedOperation = selected.length !== 1 ? null : schema.operationByID.get(selected[0]) ?? null;
|
||||
const selectedBlock = selected.length !== 1 ? null : schema.blockByID.get(-selected[0]) ?? null;
|
||||
const selectedOperation =
|
||||
selectedItems.length === 1 && selectedItems[0].nodeType === NodeType.OPERATION ? selectedItems[0] : null;
|
||||
const selectedBlock =
|
||||
selectedItems.length === 1 && selectedItems[0].nodeType === NodeType.BLOCK ? selectedItems[0] : null;
|
||||
const getLayout = useGetLayout();
|
||||
|
||||
const { updateLayout } = useUpdateLayout();
|
||||
|
@ -145,7 +148,9 @@ export function ToolbarOssGraph({
|
|||
title='Исправить позиции узлов'
|
||||
icon={<IconFixLayout size='1.25rem' className='icon-primary' />}
|
||||
onClick={handleFixLayout}
|
||||
disabled={selected.length > 1 || selected[0] > 0}
|
||||
disabled={
|
||||
selectedItems.length > 1 || (selectedItems.length > 0 && selectedItems[0].nodeType === NodeType.OPERATION)
|
||||
}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Настройки отображения'
|
||||
|
@ -181,14 +186,14 @@ export function ToolbarOssGraph({
|
|||
title='Активировать операцию'
|
||||
icon={<IconExecute size='1.25rem' className='icon-green' />}
|
||||
onClick={handleOperationExecute}
|
||||
disabled={isProcessing || selected.length !== 1 || !readyForSynthesis}
|
||||
disabled={isProcessing || selectedItems.length !== 1 || !readyForSynthesis}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml={prepareTooltip('Редактировать выбранную', 'Двойной клик')}
|
||||
aria-label='Редактировать выбранную'
|
||||
icon={<IconEdit2 size='1.25rem' className='icon-primary' />}
|
||||
onClick={handleEditItem}
|
||||
disabled={selected.length !== 1 || isProcessing}
|
||||
disabled={selectedItems.length !== 1 || isProcessing}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml={prepareTooltip('Удалить выбранную', 'Delete')}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { type Node } from 'reactflow';
|
||||
|
||||
import { useMoveItems } from '@/features/oss/backend/use-move-items';
|
||||
|
||||
import { useThrottleCallback } from '@/hooks/use-throttle-callback';
|
||||
import { useDraggingStore } from '@/stores/dragging';
|
||||
|
||||
import { useMoveItems } from '../../../backend/use-move-items';
|
||||
import { NodeType } from '../../../models/oss';
|
||||
import { useOssEdit } from '../oss-edit-context';
|
||||
|
||||
import { useOssFlow } from './oss-flow-context';
|
||||
|
@ -21,6 +21,7 @@ interface DraggingProps {
|
|||
export function useDragging({ hideContextMenu }: DraggingProps) {
|
||||
const { setContainMovement, containMovement, setNodes } = useOssFlow();
|
||||
const setIsDragging = useDraggingStore(state => state.setIsDragging);
|
||||
const isDragging = useDraggingStore(state => state.isDragging);
|
||||
const getLayout = useGetLayout();
|
||||
const { selected, schema } = useOssEdit();
|
||||
const dropTarget = useDropTarget();
|
||||
|
@ -43,7 +44,7 @@ export function useDragging({ hideContextMenu }: DraggingProps) {
|
|||
function handleDragStart(event: React.MouseEvent, target: Node) {
|
||||
if (event.shiftKey) {
|
||||
setContainMovement(true);
|
||||
applyContainMovement([target.id, ...selected.map(id => String(id))], true);
|
||||
applyContainMovement([target.id, ...selected], true);
|
||||
} else {
|
||||
setContainMovement(false);
|
||||
dropTarget.update(event);
|
||||
|
@ -61,41 +62,37 @@ export function useDragging({ hideContextMenu }: DraggingProps) {
|
|||
|
||||
function handleDragStop(event: React.MouseEvent, target: Node) {
|
||||
if (containMovement) {
|
||||
applyContainMovement([target.id, ...selected.map(id => String(id))], false);
|
||||
applyContainMovement([target.id, ...selected], false);
|
||||
} else {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const new_parent = dropTarget.evaluate(event);
|
||||
const allSelected = [...selected.filter(id => id != Number(target.id)), Number(target.id)];
|
||||
const operations = allSelected
|
||||
.filter(id => id > 0)
|
||||
.map(id => schema.operationByID.get(id))
|
||||
.filter(operation => !!operation);
|
||||
const blocks = allSelected
|
||||
.filter(id => id < 0)
|
||||
.map(id => schema.blockByID.get(-id))
|
||||
.filter(operation => !!operation);
|
||||
const parents = new Set(
|
||||
[...blocks.map(block => block.parent), ...operations.map(operation => operation.parent)].filter(id => !!id)
|
||||
);
|
||||
if (
|
||||
(parents.size !== 1 || parents.values().next().value !== new_parent) &&
|
||||
(parents.size !== 0 || new_parent !== null)
|
||||
) {
|
||||
void moveItems({
|
||||
itemID: schema.id,
|
||||
data: {
|
||||
layout: getLayout(),
|
||||
operations: operations.map(operation => operation.id),
|
||||
blocks: blocks.map(block => block.id),
|
||||
destination: new_parent
|
||||
}
|
||||
});
|
||||
if (isDragging) {
|
||||
setIsDragging(false);
|
||||
const new_parent = dropTarget.evaluate(event);
|
||||
const allSelected = [...selected.filter(id => id != target.id), target.id].map(id =>
|
||||
schema.itemByNodeID.get(id)
|
||||
);
|
||||
const parents = new Set(allSelected.map(item => item?.parent).filter(id => !!id));
|
||||
const operations = allSelected.filter(item => item?.nodeType === NodeType.OPERATION);
|
||||
const blocks = allSelected.filter(item => item?.nodeType === NodeType.BLOCK);
|
||||
if (
|
||||
(parents.size !== 1 || parents.values().next().value !== new_parent) &&
|
||||
(parents.size !== 0 || new_parent !== null)
|
||||
) {
|
||||
void moveItems({
|
||||
itemID: schema.id,
|
||||
data: {
|
||||
layout: getLayout(),
|
||||
operations: operations.map(operation => operation.id),
|
||||
blocks: blocks.map(block => block.id),
|
||||
destination: new_parent
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setIsDragging(false);
|
||||
setContainMovement(false);
|
||||
dropTarget.reset();
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { useReactFlow } from 'reactflow';
|
||||
|
||||
import { NodeType } from '@/features/oss/models/oss';
|
||||
|
||||
import { useDraggingStore } from '@/stores/dragging';
|
||||
|
||||
import { useOssEdit } from '../oss-edit-context';
|
||||
|
@ -7,7 +9,7 @@ import { useOssEdit } from '../oss-edit-context';
|
|||
/** Hook to encapsulate drop target logic. */
|
||||
export function useDropTarget() {
|
||||
const { getIntersectingNodes, screenToFlowPosition } = useReactFlow();
|
||||
const { selected, schema } = useOssEdit();
|
||||
const { selectedItems, selected, schema } = useOssEdit();
|
||||
const dropTarget = useDraggingStore(state => state.dropTarget);
|
||||
const setDropTarget = useDraggingStore(state => state.setDropTarget);
|
||||
|
||||
|
@ -19,17 +21,16 @@ export function useDropTarget() {
|
|||
width: 1,
|
||||
height: 1
|
||||
})
|
||||
.map(node => Number(node.id))
|
||||
.filter(id => id < 0 && !selected.includes(id))
|
||||
.map(id => schema.blockByID.get(-id))
|
||||
.filter(block => !!block);
|
||||
.filter(node => !selected.includes(node.id))
|
||||
.map(node => schema.itemByNodeID.get(node.id))
|
||||
.filter(item => item?.nodeType === NodeType.BLOCK);
|
||||
|
||||
if (blocks.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const successors = schema.hierarchy.expandAllOutputs([...selected]).filter(id => id < 0);
|
||||
blocks = blocks.filter(block => !successors.includes(-block.id));
|
||||
const successors = schema.hierarchy.expandAllOutputs(selectedItems.map(item => item.nodeID));
|
||||
blocks = blocks.filter(block => !successors.includes(block.nodeID));
|
||||
if (blocks.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -18,13 +18,13 @@ export function useGetLayout() {
|
|||
operations: nodes
|
||||
.filter(node => node.type !== 'block')
|
||||
.map(node => ({
|
||||
id: Number(node.id),
|
||||
id: schema.itemByNodeID.get(node.id)!.id,
|
||||
...computeAbsolutePosition(node, schema, nodeById)
|
||||
})),
|
||||
blocks: nodes
|
||||
.filter(node => node.type === 'block')
|
||||
.map(node => ({
|
||||
id: -Number(node.id),
|
||||
id: schema.itemByNodeID.get(node.id)!.id,
|
||||
...computeAbsolutePosition(node, schema, nodeById),
|
||||
width: node.width ?? BLOCK_NODE_MIN_WIDTH,
|
||||
height: node.height ?? BLOCK_NODE_MIN_HEIGHT
|
||||
|
@ -35,7 +35,7 @@ export function useGetLayout() {
|
|||
|
||||
// ------- Internals -------
|
||||
function computeAbsolutePosition(target: Node, schema: IOperationSchema, nodeById: Map<string, Node>): Position2D {
|
||||
const nodes = schema.hierarchy.expandAllInputs([Number(target.id)]);
|
||||
const nodes = schema.hierarchy.expandAllInputs([target.id]);
|
||||
let x = target.position.x;
|
||||
let y = target.position.y;
|
||||
for (const nodeID of nodes) {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { createContext, use } from 'react';
|
||||
|
||||
import { type IOperation, type IOperationSchema } from '../../models/oss';
|
||||
import { type IOperation, type IOperationSchema, type IOssItem } from '../../models/oss';
|
||||
|
||||
export const OssTabID = {
|
||||
CARD: 0,
|
||||
|
@ -12,7 +12,8 @@ export type OssTabID = (typeof OssTabID)[keyof typeof OssTabID];
|
|||
|
||||
interface IOssEditContext {
|
||||
schema: IOperationSchema;
|
||||
selected: number[];
|
||||
selected: string[];
|
||||
selectedItems: IOssItem[];
|
||||
|
||||
isOwned: boolean;
|
||||
isMutable: boolean;
|
||||
|
@ -22,7 +23,7 @@ interface IOssEditContext {
|
|||
|
||||
canDeleteOperation: (target: IOperation) => boolean;
|
||||
deleteSchema: () => void;
|
||||
setSelected: React.Dispatch<React.SetStateAction<number[]>>;
|
||||
setSelected: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
}
|
||||
|
||||
export const OssEditContext = createContext<IOssEditContext | null>(null);
|
||||
|
|
|
@ -38,7 +38,8 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
|
|||
const isMutable = role > UserRole.READER && !schema.read_only;
|
||||
const isEditor = !!user.id && schema.editors.includes(user.id);
|
||||
|
||||
const [selected, setSelected] = useState<number[]>([]);
|
||||
const [selected, setSelected] = useState<string[]>([]);
|
||||
const selectedItems = selected.map(id => schema.itemByNodeID.get(id)).filter(item => !!item);
|
||||
|
||||
const { deleteItem } = useDeleteItem();
|
||||
|
||||
|
@ -92,6 +93,7 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
|
|||
value={{
|
||||
schema,
|
||||
selected,
|
||||
selectedItems,
|
||||
|
||||
isOwned,
|
||||
isMutable,
|
||||
|
|
|
@ -5,41 +5,41 @@
|
|||
/**
|
||||
* Represents single node of a {@link Graph}, as implemented by storing outgoing and incoming connections.
|
||||
*/
|
||||
export class GraphNode {
|
||||
export class GraphNode<NodeID> {
|
||||
/** Unique identifier of the node. */
|
||||
id: number;
|
||||
id: NodeID;
|
||||
/** List of outgoing nodes. */
|
||||
outputs: number[];
|
||||
outputs: NodeID[];
|
||||
/** List of incoming nodes. */
|
||||
inputs: number[];
|
||||
inputs: NodeID[];
|
||||
|
||||
constructor(id: number) {
|
||||
constructor(id: NodeID) {
|
||||
this.id = id;
|
||||
this.outputs = [];
|
||||
this.inputs = [];
|
||||
}
|
||||
|
||||
clone(): GraphNode {
|
||||
clone(): GraphNode<NodeID> {
|
||||
const result = new GraphNode(this.id);
|
||||
result.outputs = [...this.outputs];
|
||||
result.inputs = [...this.inputs];
|
||||
return result;
|
||||
}
|
||||
|
||||
addOutput(node: number): void {
|
||||
addOutput(node: NodeID): void {
|
||||
this.outputs.push(node);
|
||||
}
|
||||
|
||||
addInput(node: number): void {
|
||||
addInput(node: NodeID): void {
|
||||
this.inputs.push(node);
|
||||
}
|
||||
|
||||
removeInput(target: number): number | null {
|
||||
removeInput(target: NodeID): NodeID | null {
|
||||
const index = this.inputs.findIndex(node => node === target);
|
||||
return index > -1 ? this.inputs.splice(index, 1)[0] : null;
|
||||
}
|
||||
|
||||
removeOutput(target: number): number | null {
|
||||
removeOutput(target: NodeID): NodeID | null {
|
||||
const index = this.outputs.findIndex(node => node === target);
|
||||
return index > -1 ? this.outputs.splice(index, 1)[0] : null;
|
||||
}
|
||||
|
@ -50,11 +50,11 @@ export class GraphNode {
|
|||
*
|
||||
* This class is optimized for TermGraph use case and not supposed to be used as generic graph implementation.
|
||||
*/
|
||||
export class Graph {
|
||||
export class Graph<NodeID = number> {
|
||||
/** Map of nodes. */
|
||||
nodes = new Map<number, GraphNode>();
|
||||
nodes = new Map<NodeID, GraphNode<NodeID>>();
|
||||
|
||||
constructor(arr?: number[][]) {
|
||||
constructor(arr?: NodeID[][]) {
|
||||
if (!arr) {
|
||||
return;
|
||||
}
|
||||
|
@ -67,17 +67,17 @@ export class Graph {
|
|||
});
|
||||
}
|
||||
|
||||
clone(): Graph {
|
||||
const result = new Graph();
|
||||
clone(): Graph<NodeID> {
|
||||
const result = new Graph<NodeID>();
|
||||
this.nodes.forEach(node => result.nodes.set(node.id, node.clone()));
|
||||
return result;
|
||||
}
|
||||
|
||||
at(target: number): GraphNode | undefined {
|
||||
at(target: NodeID): GraphNode<NodeID> | undefined {
|
||||
return this.nodes.get(target);
|
||||
}
|
||||
|
||||
addNode(target: number): GraphNode {
|
||||
addNode(target: NodeID): GraphNode<NodeID> {
|
||||
let node = this.nodes.get(target);
|
||||
if (!node) {
|
||||
node = new GraphNode(target);
|
||||
|
@ -86,11 +86,11 @@ export class Graph {
|
|||
return node;
|
||||
}
|
||||
|
||||
hasNode(target: number): boolean {
|
||||
hasNode(target: NodeID): boolean {
|
||||
return !!this.nodes.get(target);
|
||||
}
|
||||
|
||||
removeNode(target: number): void {
|
||||
removeNode(target: NodeID): void {
|
||||
this.nodes.forEach(node => {
|
||||
node.removeInput(target);
|
||||
node.removeOutput(target);
|
||||
|
@ -98,7 +98,7 @@ export class Graph {
|
|||
this.nodes.delete(target);
|
||||
}
|
||||
|
||||
foldNode(target: number): void {
|
||||
foldNode(target: NodeID): void {
|
||||
const nodeToRemove = this.nodes.get(target);
|
||||
if (!nodeToRemove) {
|
||||
return;
|
||||
|
@ -111,8 +111,8 @@ export class Graph {
|
|||
this.removeNode(target);
|
||||
}
|
||||
|
||||
removeIsolated(): GraphNode[] {
|
||||
const result: GraphNode[] = [];
|
||||
removeIsolated(): GraphNode<NodeID>[] {
|
||||
const result: GraphNode<NodeID>[] = [];
|
||||
this.nodes.forEach(node => {
|
||||
if (node.outputs.length === 0 && node.inputs.length === 0) {
|
||||
result.push(node);
|
||||
|
@ -122,7 +122,7 @@ export class Graph {
|
|||
return result;
|
||||
}
|
||||
|
||||
addEdge(source: number, destination: number): void {
|
||||
addEdge(source: NodeID, destination: NodeID): void {
|
||||
if (this.hasEdge(source, destination)) {
|
||||
return;
|
||||
}
|
||||
|
@ -132,7 +132,7 @@ export class Graph {
|
|||
destinationNode.addInput(sourceNode.id);
|
||||
}
|
||||
|
||||
removeEdge(source: number, destination: number): void {
|
||||
removeEdge(source: NodeID, destination: NodeID): void {
|
||||
const sourceNode = this.nodes.get(source);
|
||||
const destinationNode = this.nodes.get(destination);
|
||||
if (sourceNode && destinationNode) {
|
||||
|
@ -141,7 +141,7 @@ export class Graph {
|
|||
}
|
||||
}
|
||||
|
||||
hasEdge(source: number, destination: number): boolean {
|
||||
hasEdge(source: NodeID, destination: NodeID): boolean {
|
||||
const sourceNode = this.nodes.get(source);
|
||||
if (!sourceNode) {
|
||||
return false;
|
||||
|
@ -149,8 +149,8 @@ export class Graph {
|
|||
return !!sourceNode.outputs.find(id => id === destination);
|
||||
}
|
||||
|
||||
expandOutputs(origin: number[]): number[] {
|
||||
const result: number[] = [];
|
||||
expandOutputs(origin: NodeID[]): NodeID[] {
|
||||
const result: NodeID[] = [];
|
||||
origin.forEach(id => {
|
||||
const node = this.nodes.get(id);
|
||||
if (node) {
|
||||
|
@ -164,8 +164,8 @@ export class Graph {
|
|||
return result;
|
||||
}
|
||||
|
||||
expandInputs(origin: number[]): number[] {
|
||||
const result: number[] = [];
|
||||
expandInputs(origin: NodeID[]): NodeID[] {
|
||||
const result: NodeID[] = [];
|
||||
origin.forEach(id => {
|
||||
const node = this.nodes.get(id);
|
||||
if (node) {
|
||||
|
@ -179,13 +179,13 @@ export class Graph {
|
|||
return result;
|
||||
}
|
||||
|
||||
expandAllOutputs(origin: number[]): number[] {
|
||||
const result: number[] = this.expandOutputs(origin);
|
||||
expandAllOutputs(origin: NodeID[]): NodeID[] {
|
||||
const result: NodeID[] = this.expandOutputs(origin);
|
||||
if (result.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const marked = new Map<number, boolean>();
|
||||
const marked = new Map<NodeID, boolean>();
|
||||
origin.forEach(id => marked.set(id, true));
|
||||
let position = 0;
|
||||
while (position < result.length) {
|
||||
|
@ -203,13 +203,13 @@ export class Graph {
|
|||
return result;
|
||||
}
|
||||
|
||||
expandAllInputs(origin: number[]): number[] {
|
||||
const result: number[] = this.expandInputs(origin);
|
||||
expandAllInputs(origin: NodeID[]): NodeID[] {
|
||||
const result: NodeID[] = this.expandInputs(origin);
|
||||
if (result.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const marked = new Map<number, boolean>();
|
||||
const marked = new Map<NodeID, boolean>();
|
||||
origin.forEach(id => marked.set(id, true));
|
||||
let position = 0;
|
||||
while (position < result.length) {
|
||||
|
@ -227,8 +227,8 @@ export class Graph {
|
|||
return result;
|
||||
}
|
||||
|
||||
maximizePart(origin: number[]): number[] {
|
||||
const outputs: number[] = this.expandAllOutputs(origin);
|
||||
maximizePart(origin: NodeID[]): NodeID[] {
|
||||
const outputs: NodeID[] = this.expandAllOutputs(origin);
|
||||
const result = [...origin];
|
||||
this.topologicalOrder()
|
||||
.filter(id => outputs.includes(id))
|
||||
|
@ -241,10 +241,10 @@ export class Graph {
|
|||
return result;
|
||||
}
|
||||
|
||||
topologicalOrder(): number[] {
|
||||
const result: number[] = [];
|
||||
const marked = new Set<number>();
|
||||
const nodeStack: number[] = [];
|
||||
topologicalOrder(): NodeID[] {
|
||||
const result: NodeID[] = [];
|
||||
const marked = new Set<NodeID>();
|
||||
const nodeStack: NodeID[] = [];
|
||||
this.nodes.forEach(node => {
|
||||
if (marked.has(node.id)) {
|
||||
return;
|
||||
|
@ -275,12 +275,12 @@ export class Graph {
|
|||
|
||||
transitiveReduction() {
|
||||
const order = this.topologicalOrder();
|
||||
const marked = new Map<number, boolean>();
|
||||
const marked = new Map<NodeID, boolean>();
|
||||
order.forEach(nodeID => {
|
||||
if (marked.get(nodeID)) {
|
||||
return;
|
||||
}
|
||||
const stack: { id: number; parents: number[] }[] = [];
|
||||
const stack: { id: NodeID; parents: NodeID[] }[] = [];
|
||||
stack.push({ id: nodeID, parents: [] });
|
||||
while (stack.length > 0) {
|
||||
const item = stack.splice(0, 1)[0];
|
||||
|
@ -299,20 +299,20 @@ export class Graph {
|
|||
/**
|
||||
* Finds a cycle in the graph.
|
||||
*
|
||||
* @returns {number[] | null} The cycle if found, otherwise `null`.
|
||||
* @returns {NodeID[] | null} The cycle if found, otherwise `null`.
|
||||
* Uses non-recursive DFS.
|
||||
*/
|
||||
findCycle(): number[] | null {
|
||||
const visited = new Set<number>();
|
||||
const nodeStack = new Set<number>();
|
||||
const parents = new Map<number, number>();
|
||||
findCycle(): NodeID[] | null {
|
||||
const visited = new Set<NodeID>();
|
||||
const nodeStack = new Set<NodeID>();
|
||||
const parents = new Map<NodeID, NodeID>();
|
||||
|
||||
for (const nodeId of this.nodes.keys()) {
|
||||
if (visited.has(nodeId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const callStack: { nodeId: number; parentId: number | null }[] = [];
|
||||
const callStack: { nodeId: NodeID; parentId: NodeID | null }[] = [];
|
||||
callStack.push({ nodeId: nodeId, parentId: null });
|
||||
while (callStack.length > 0) {
|
||||
const { nodeId, parentId } = callStack[callStack.length - 1];
|
||||
|
@ -336,7 +336,7 @@ export class Graph {
|
|||
if (!nodeStack.has(child)) {
|
||||
continue;
|
||||
}
|
||||
const cycle: number[] = [];
|
||||
const cycle: NodeID[] = [];
|
||||
let current = nodeId;
|
||||
cycle.push(child);
|
||||
while (current !== child) {
|
||||
|
|
Loading…
Reference in New Issue
Block a user