diff --git a/rsconcept/frontend/src/features/oss/dialogs/dlg-create-block/dlg-create-block.tsx b/rsconcept/frontend/src/features/oss/dialogs/dlg-create-block/dlg-create-block.tsx index 8e13b8a6..c0ff781b 100644 --- a/rsconcept/frontend/src/features/oss/dialogs/dlg-create-block/dlg-create-block.tsx +++ b/rsconcept/frontend/src/features/oss/dialogs/dlg-create-block/dlg-create-block.tsx @@ -10,18 +10,16 @@ import { ModalForm } from '@/components/modal'; import { TabLabel, TabList, TabPanel, Tabs } from '@/components/tabs'; import { useDialogsStore } from '@/stores/dialogs'; -import { type ICreateBlockDTO, type IOssLayout, schemaCreateBlock } from '../../backend/types'; +import { type ICreateBlockDTO, schemaCreateBlock } from '../../backend/types'; import { useCreateBlock } from '../../backend/use-create-block'; -import { type IOperationSchema } from '../../models/oss'; -import { calculateNewBlockPosition } from '../../models/oss-api'; +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'; import { TabBlockCard } from './tab-block-card'; import { TabBlockChildren } from './tab-block-children'; export interface DlgCreateBlockProps { - oss: IOperationSchema; - layout: IOssLayout; + manager: LayoutManager; initialInputs: number[]; defaultX: number; defaultY: number; @@ -37,7 +35,7 @@ export type TabID = (typeof TabID)[keyof typeof TabID]; export function DlgCreateBlock() { const { createBlock } = useCreateBlock(); - const { oss, layout, initialInputs, onCreate, defaultX, defaultY } = useDialogsStore( + const { manager, initialInputs, onCreate, defaultX, defaultY } = useDialogsStore( state => state.props as DlgCreateBlockProps ); @@ -55,21 +53,21 @@ export function DlgCreateBlock() { height: BLOCK_NODE_MIN_HEIGHT, children_blocks: initialInputs.filter(id => id < 0).map(id => -id), children_operations: initialInputs.filter(id => id > 0), - layout: layout + layout: manager.layout }, mode: 'onChange' }); const title = useWatch({ control: methods.control, name: 'item_data.title' }); const [activeTab, setActiveTab] = useState(TabID.CARD); - const isValid = !!title && !oss.blocks.some(block => block.title === title); + const isValid = !!title && !manager.oss.blocks.some(block => block.title === title); function onSubmit(data: ICreateBlockDTO) { - const rectangle = calculateNewBlockPosition(data, layout); + const rectangle = manager.calculateNewBlockPosition(data); data.position_x = rectangle.x; data.position_y = rectangle.y; data.width = rectangle.width; data.height = rectangle.height; - void createBlock({ itemID: oss.id, data: data }).then(response => onCreate?.(response.new_block.id)); + void createBlock({ itemID: manager.oss.id, data: data }).then(response => onCreate?.(response.new_block.id)); } return ( diff --git a/rsconcept/frontend/src/features/oss/dialogs/dlg-create-block/tab-block-card.tsx b/rsconcept/frontend/src/features/oss/dialogs/dlg-create-block/tab-block-card.tsx index 2e551d24..baac56ed 100644 --- a/rsconcept/frontend/src/features/oss/dialogs/dlg-create-block/tab-block-card.tsx +++ b/rsconcept/frontend/src/features/oss/dialogs/dlg-create-block/tab-block-card.tsx @@ -11,7 +11,7 @@ import { SelectParent } from '../../components/select-parent'; import { type DlgCreateBlockProps } from './dlg-create-block'; export function TabBlockCard() { - const { oss } = useDialogsStore(state => state.props as DlgCreateBlockProps); + const { manager } = useDialogsStore(state => state.props as DlgCreateBlockProps); const { register, control, @@ -31,8 +31,8 @@ export function TabBlockCard() { control={control} render={({ field }) => ( field.onChange(value ? value.id : null)} /> diff --git a/rsconcept/frontend/src/features/oss/dialogs/dlg-create-block/tab-block-children.tsx b/rsconcept/frontend/src/features/oss/dialogs/dlg-create-block/tab-block-children.tsx index e190dbb7..a0ffe3fb 100644 --- a/rsconcept/frontend/src/features/oss/dialogs/dlg-create-block/tab-block-children.tsx +++ b/rsconcept/frontend/src/features/oss/dialogs/dlg-create-block/tab-block-children.tsx @@ -11,7 +11,7 @@ import { type DlgCreateBlockProps } from './dlg-create-block'; export function TabBlockChildren() { const { setValue, control } = useFormContext(); - const { oss } = useDialogsStore(state => state.props as DlgCreateBlockProps); + const { manager } = useDialogsStore(state => state.props as DlgCreateBlockProps); const children_blocks = useWatch({ control, name: 'children_blocks' }); const children_operations = useWatch({ control, name: 'children_operations' }); @@ -33,7 +33,12 @@ export function TabBlockChildren() { return (
); } diff --git a/rsconcept/frontend/src/features/oss/dialogs/dlg-create-operation/dlg-create-operation.tsx b/rsconcept/frontend/src/features/oss/dialogs/dlg-create-operation/dlg-create-operation.tsx index 03e58bcb..73d68687 100644 --- a/rsconcept/frontend/src/features/oss/dialogs/dlg-create-operation/dlg-create-operation.tsx +++ b/rsconcept/frontend/src/features/oss/dialogs/dlg-create-operation/dlg-create-operation.tsx @@ -10,18 +10,16 @@ import { ModalForm } from '@/components/modal'; import { TabLabel, TabList, TabPanel, Tabs } from '@/components/tabs'; import { useDialogsStore } from '@/stores/dialogs'; -import { type ICreateOperationDTO, type IOssLayout, OperationType, schemaCreateOperation } from '../../backend/types'; +import { type ICreateOperationDTO, OperationType, schemaCreateOperation } from '../../backend/types'; import { useCreateOperation } from '../../backend/use-create-operation'; import { describeOperationType, labelOperationType } from '../../labels'; -import { type IOperationSchema } from '../../models/oss'; -import { calculateNewOperationPosition } from '../../models/oss-api'; +import { type LayoutManager } from '../../models/oss-layout-api'; import { TabInputOperation } from './tab-input-operation'; import { TabSynthesisOperation } from './tab-synthesis-operation'; export interface DlgCreateOperationProps { - oss: IOperationSchema; - layout: IOssLayout; + manager: LayoutManager; initialParent: number | null; initialInputs: number[]; defaultX: number; @@ -38,7 +36,7 @@ export type TabID = (typeof TabID)[keyof typeof TabID]; export function DlgCreateOperation() { const { createOperation } = useCreateOperation(); - const { oss, layout, initialInputs, initialParent, onCreate, defaultX, defaultY } = useDialogsStore( + const { manager, initialInputs, initialParent, onCreate, defaultX, defaultY } = useDialogsStore( state => state.props as DlgCreateOperationProps ); @@ -57,19 +55,21 @@ export function DlgCreateOperation() { position_y: defaultY, arguments: initialInputs, create_schema: false, - layout: layout + layout: manager.layout }, mode: 'onChange' }); const alias = useWatch({ control: methods.control, name: 'item_data.alias' }); const [activeTab, setActiveTab] = useState(initialInputs.length === 0 ? TabID.INPUT : TabID.SYNTHESIS); - const isValid = !!alias && !oss.operations.some(operation => operation.alias === alias); + const isValid = !!alias && !manager.oss.operations.some(operation => operation.alias === alias); function onSubmit(data: ICreateOperationDTO) { - const target = calculateNewOperationPosition(oss, data, layout); + const target = manager.calculateNewOperationPosition(data); data.position_x = target.x; data.position_y = target.y; - void createOperation({ itemID: oss.id, data: data }).then(response => onCreate?.(response.new_operation.id)); + void createOperation({ itemID: manager.oss.id, data: data }).then(response => + onCreate?.(response.new_operation.id) + ); } function handleSelectTab(newTab: TabID, last: TabID) { diff --git a/rsconcept/frontend/src/features/oss/dialogs/dlg-create-operation/tab-input-operation.tsx b/rsconcept/frontend/src/features/oss/dialogs/dlg-create-operation/tab-input-operation.tsx index 2afbadaa..b74f121a 100644 --- a/rsconcept/frontend/src/features/oss/dialogs/dlg-create-operation/tab-input-operation.tsx +++ b/rsconcept/frontend/src/features/oss/dialogs/dlg-create-operation/tab-input-operation.tsx @@ -18,9 +18,9 @@ import { sortItemsForOSS } from '../../models/oss-api'; import { type DlgCreateOperationProps } from './dlg-create-operation'; export function TabInputOperation() { - const { oss } = useDialogsStore(state => state.props as DlgCreateOperationProps); + const { manager } = useDialogsStore(state => state.props as DlgCreateOperationProps); const { items: libraryItems } = useLibrary(); - const sortedItems = sortItemsForOSS(oss, libraryItems); + const sortedItems = sortItemsForOSS(manager.oss, libraryItems); const { register, @@ -31,7 +31,7 @@ export function TabInputOperation() { const createSchema = useWatch({ control, name: 'create_schema' }); function baseFilter(item: ILibraryItem) { - return !oss.schemas.includes(item.id); + return !manager.oss.schemas.includes(item.id); } function handleChangeCreateSchema(value: boolean) { @@ -75,8 +75,8 @@ export function TabInputOperation() { control={control} render={({ field }) => ( field.onChange(value ? value.id : null)} /> diff --git a/rsconcept/frontend/src/features/oss/dialogs/dlg-create-operation/tab-synthesis-operation.tsx b/rsconcept/frontend/src/features/oss/dialogs/dlg-create-operation/tab-synthesis-operation.tsx index 70df918d..5a9c583c 100644 --- a/rsconcept/frontend/src/features/oss/dialogs/dlg-create-operation/tab-synthesis-operation.tsx +++ b/rsconcept/frontend/src/features/oss/dialogs/dlg-create-operation/tab-synthesis-operation.tsx @@ -10,7 +10,7 @@ import { SelectParent } from '../../components/select-parent'; import { type DlgCreateOperationProps } from './dlg-create-operation'; export function TabSynthesisOperation() { - const { oss } = useDialogsStore(state => state.props as DlgCreateOperationProps); + const { manager } = useDialogsStore(state => state.props as DlgCreateOperationProps); const { register, control, @@ -40,8 +40,8 @@ export function TabSynthesisOperation() { control={control} render={({ field }) => ( field.onChange(value ? value.id : null)} /> @@ -64,7 +64,7 @@ export function TabSynthesisOperation() { name='arguments' control={control} render={({ field }) => ( - + )} /> diff --git a/rsconcept/frontend/src/features/oss/dialogs/dlg-edit-block.tsx b/rsconcept/frontend/src/features/oss/dialogs/dlg-edit-block.tsx index aae89a6b..27459f41 100644 --- a/rsconcept/frontend/src/features/oss/dialogs/dlg-edit-block.tsx +++ b/rsconcept/frontend/src/features/oss/dialogs/dlg-edit-block.tsx @@ -7,19 +7,19 @@ import { TextArea, TextInput } from '@/components/input'; import { ModalForm } from '@/components/modal'; import { useDialogsStore } from '@/stores/dialogs'; -import { type IOssLayout, type IUpdateBlockDTO, schemaUpdateBlock } from '../backend/types'; +import { type IUpdateBlockDTO, schemaUpdateBlock } from '../backend/types'; import { useUpdateBlock } from '../backend/use-update-block'; import { SelectParent } from '../components/select-parent'; -import { type IBlock, type IOperationSchema } from '../models/oss'; +import { type IBlock } from '../models/oss'; +import { type LayoutManager } from '../models/oss-layout-api'; export interface DlgEditBlockProps { - oss: IOperationSchema; + manager: LayoutManager; target: IBlock; - layout: IOssLayout; } export function DlgEditBlock() { - const { oss, target, layout } = useDialogsStore(state => state.props as DlgEditBlockProps); + const { manager, target } = useDialogsStore(state => state.props as DlgEditBlockProps); const { updateBlock } = useUpdateBlock(); const { @@ -36,13 +36,13 @@ export function DlgEditBlock() { description: target.description, parent: target.parent }, - layout: layout + layout: manager.layout }, mode: 'onChange' }); function onSubmit(data: IUpdateBlockDTO) { - return updateBlock({ itemID: oss.id, data }); + return updateBlock({ itemID: manager.oss.id, data }); } return ( @@ -64,8 +64,8 @@ export function DlgEditBlock() { control={control} render={({ field }) => ( block.id !== target.id)} - value={field.value ? oss.blockByID.get(field.value) ?? null : null} + items={manager.oss.blocks.filter(block => block.id !== target.id)} + value={field.value ? manager.oss.blockByID.get(field.value) ?? null : null} placeholder='Блок содержания не выбран' onChange={value => field.onChange(value ? value.id : null)} /> diff --git a/rsconcept/frontend/src/features/oss/dialogs/dlg-edit-operation/dlg-edit-operation.tsx b/rsconcept/frontend/src/features/oss/dialogs/dlg-edit-operation/dlg-edit-operation.tsx index 37a0c58c..492ad928 100644 --- a/rsconcept/frontend/src/features/oss/dialogs/dlg-edit-operation/dlg-edit-operation.tsx +++ b/rsconcept/frontend/src/features/oss/dialogs/dlg-edit-operation/dlg-edit-operation.tsx @@ -11,18 +11,18 @@ import { ModalForm } from '@/components/modal'; import { TabLabel, TabList, TabPanel, Tabs } from '@/components/tabs'; import { useDialogsStore } from '@/stores/dialogs'; -import { type IOssLayout, type IUpdateOperationDTO, OperationType, schemaUpdateOperation } from '../../backend/types'; +import { type IUpdateOperationDTO, OperationType, schemaUpdateOperation } from '../../backend/types'; import { useUpdateOperation } from '../../backend/use-update-operation'; -import { type IOperation, type IOperationSchema } from '../../models/oss'; +import { type IOperation } from '../../models/oss'; +import { type LayoutManager } from '../../models/oss-layout-api'; import { TabArguments } from './tab-arguments'; import { TabOperation } from './tab-operation'; import { TabSynthesis } from './tab-synthesis'; export interface DlgEditOperationProps { - oss: IOperationSchema; + manager: LayoutManager; target: IOperation; - layout: IOssLayout; } export const TabID = { @@ -33,7 +33,7 @@ export const TabID = { export type TabID = (typeof TabID)[keyof typeof TabID]; export function DlgEditOperation() { - const { oss, target, layout } = useDialogsStore(state => state.props as DlgEditOperationProps); + const { manager, target } = useDialogsStore(state => state.props as DlgEditOperationProps); const { updateOperation } = useUpdateOperation(); const methods = useForm({ @@ -51,14 +51,17 @@ export function DlgEditOperation() { original: sub.original, substitution: sub.substitution })), - layout: layout + layout: manager.layout }, mode: 'onChange' }); const [activeTab, setActiveTab] = useState(TabID.CARD); function onSubmit(data: IUpdateOperationDTO) { - return updateOperation({ itemID: oss.id, data }); + // if (data.item_data.parent !== target.parent) { + // data.layout = updateLayoutOnOperationChange(data.target, data.item_data.parent, data.layout); + // } + return updateOperation({ itemID: manager.oss.id, data }); } return ( diff --git a/rsconcept/frontend/src/features/oss/dialogs/dlg-edit-operation/tab-arguments.tsx b/rsconcept/frontend/src/features/oss/dialogs/dlg-edit-operation/tab-arguments.tsx index 3f5849fb..a5edb2f3 100644 --- a/rsconcept/frontend/src/features/oss/dialogs/dlg-edit-operation/tab-arguments.tsx +++ b/rsconcept/frontend/src/features/oss/dialogs/dlg-edit-operation/tab-arguments.tsx @@ -11,9 +11,9 @@ import { type DlgEditOperationProps } from './dlg-edit-operation'; export function TabArguments() { const { control, setValue } = useFormContext(); - const { oss, target } = useDialogsStore(state => state.props as DlgEditOperationProps); - const potentialCycle = [target.id, ...oss.graph.expandAllOutputs([target.id])]; - const filtered = oss.operations.filter(item => !potentialCycle.includes(item.id)); + const { manager, target } = useDialogsStore(state => state.props as DlgEditOperationProps); + const potentialCycle = [target.id, ...manager.oss.graph.expandAllOutputs([target.id])]; + const filtered = manager.oss.operations.filter(item => !potentialCycle.includes(item.id)); function handleChangeArguments(prev: number[], newValue: number[]) { setValue('arguments', newValue, { shouldValidate: true }); diff --git a/rsconcept/frontend/src/features/oss/dialogs/dlg-edit-operation/tab-operation.tsx b/rsconcept/frontend/src/features/oss/dialogs/dlg-edit-operation/tab-operation.tsx index 356262b9..290091c1 100644 --- a/rsconcept/frontend/src/features/oss/dialogs/dlg-edit-operation/tab-operation.tsx +++ b/rsconcept/frontend/src/features/oss/dialogs/dlg-edit-operation/tab-operation.tsx @@ -9,7 +9,7 @@ import { SelectParent } from '../../components/select-parent'; import { type DlgEditOperationProps } from './dlg-edit-operation'; export function TabOperation() { - const { oss } = useDialogsStore(state => state.props as DlgEditOperationProps); + const { manager } = useDialogsStore(state => state.props as DlgEditOperationProps); const { register, control, @@ -37,8 +37,8 @@ export function TabOperation() { control={control} render={({ field }) => ( field.onChange(value ? value.id : null)} /> diff --git a/rsconcept/frontend/src/features/oss/dialogs/dlg-edit-operation/tab-synthesis.tsx b/rsconcept/frontend/src/features/oss/dialogs/dlg-edit-operation/tab-synthesis.tsx index 5cf97c4a..ae923b99 100644 --- a/rsconcept/frontend/src/features/oss/dialogs/dlg-edit-operation/tab-synthesis.tsx +++ b/rsconcept/frontend/src/features/oss/dialogs/dlg-edit-operation/tab-synthesis.tsx @@ -14,13 +14,13 @@ import { SubstitutionValidator } from '../../models/oss-api'; import { type DlgEditOperationProps } from './dlg-edit-operation'; export function TabSynthesis() { - const { oss } = useDialogsStore(state => state.props as DlgEditOperationProps); + const { manager } = useDialogsStore(state => state.props as DlgEditOperationProps); const { control } = useFormContext(); const inputs = useWatch({ control, name: 'arguments' }); const substitutions = useWatch({ control, name: 'substitutions' }); const schemasIDs = inputs - .map(id => oss.operationByID.get(id)!) + .map(id => manager.oss.operationByID.get(id)!) .map(operation => operation.result) .filter(id => id !== null); const schemas = useRSForms(schemasIDs); diff --git a/rsconcept/frontend/src/features/oss/models/oss-api.ts b/rsconcept/frontend/src/features/oss/models/oss-api.ts index 5387c67a..ec506be1 100644 --- a/rsconcept/frontend/src/features/oss/models/oss-api.ts +++ b/rsconcept/frontend/src/features/oss/models/oss-api.ts @@ -22,17 +22,9 @@ import { import { infoMsg } from '@/utils/labels'; import { Graph } from '../../../models/graph'; -import { type ICreateBlockDTO, type ICreateOperationDTO, type IOssLayout } from '../backend/types'; import { describeSubstitutionError } from '../labels'; -import { OPERATION_NODE_HEIGHT, OPERATION_NODE_WIDTH } from '../pages/oss-page/editor-oss-graph/graph/node-core'; import { type IOperationSchema, type IOssItem, SubstitutionErrorType } from './oss'; -import { type Position2D, type Rectangle2D } from './oss-layout'; - -export const GRID_SIZE = 10; // pixels - size of OSS grid -const MIN_DISTANCE = 2 * GRID_SIZE; // pixels - minimum distance between nodes -const DISTANCE_X = 180; // pixels - insert x-distance between node centers -const DISTANCE_Y = 100; // pixels - insert y-distance between node centers const STARTING_SUB_INDEX = 900; // max semantic index for starting substitution @@ -477,100 +469,3 @@ export function getRelocateCandidates( const unreachable = schema.graph.expandAllOutputs(unreachableBases); return addedCst.filter(cst => !unreachable.includes(cst.id)); } - -/** Calculate insert position for a new {@link IOperation} */ -export function calculateNewOperationPosition( - oss: IOperationSchema, - data: ICreateOperationDTO, - layout: IOssLayout -): Position2D { - // TODO: check parent node - - const result = { x: data.position_x, y: data.position_y }; - const operations = layout.operations; - if (operations.length === 0) { - return result; - } - - if (data.arguments.length === 0) { - let inputsPositions = operations.filter(pos => - 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; - } 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; - } - - 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; - } - } while (flagIntersect); - return result; -} - -/** Calculate insert position for a new {@link IBlock} */ -export function calculateNewBlockPosition(data: ICreateBlockDTO, layout: IOssLayout): Rectangle2D { - const block_nodes = data.children_blocks - .map(id => layout.blocks.find(block => block.id === id)) - .filter(node => !!node); - const operation_nodes = data.children_operations - .map(id => layout.operations.find(operation => operation.id === id)) - .filter(node => !!node); - - if (block_nodes.length === 0 && operation_nodes.length === 0) { - return { x: data.position_x, y: data.position_y, width: data.width, height: data.height }; - } - - 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); - } - - console.log('left, top, right, bottom', left, top, right, bottom); - - 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 - }; -} diff --git a/rsconcept/frontend/src/features/oss/models/oss-layout-api.ts b/rsconcept/frontend/src/features/oss/models/oss-layout-api.ts new file mode 100644 index 00000000..83d63db4 --- /dev/null +++ b/rsconcept/frontend/src/features/oss/models/oss-layout-api.ts @@ -0,0 +1,120 @@ +import { type ICreateBlockDTO, type ICreateOperationDTO, type IOssLayout } from '../backend/types'; + +import { type IOperationSchema } from './oss'; +import { type Position2D, type Rectangle2D } from './oss-layout'; + +export const GRID_SIZE = 10; // pixels - size of OSS grid +const MIN_DISTANCE = 2 * GRID_SIZE; // pixels - minimum distance between nodes +const DISTANCE_X = 180; // pixels - insert x-distance between node centers +const DISTANCE_Y = 100; // pixels - insert y-distance between node centers + +const OPERATION_NODE_WIDTH = 150; +const OPERATION_NODE_HEIGHT = 40; + +/** Layout manipulations for {@link IOperationSchema}. */ +export class LayoutManager { + public oss: IOperationSchema; + public layout: IOssLayout; + + constructor(oss: IOperationSchema, layout?: IOssLayout) { + this.oss = oss; + if (layout) { + this.layout = layout; + } else { + this.layout = this.oss.layout; + } + } + + /** 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 }; + const operations = this.layout.operations; + 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; + } 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; + } + + 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; + } + } while (flagIntersect); + return result; + } + + /** Calculate insert position for a new {@link IBlock} */ + calculateNewBlockPosition(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); + + if (block_nodes.length === 0 && operation_nodes.length === 0) { + return { x: data.position_x, y: data.position_y, width: data.width, height: data.height }; + } + + 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); + } + + console.log('left, top, right, bottom', left, top, right, bottom); + + 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 + }; + } +} diff --git a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/context-menu.tsx b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/context-menu.tsx index e29e5be2..0ae15167 100644 --- a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/context-menu.tsx +++ b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/context-menu.tsx @@ -2,11 +2,10 @@ import { useRef } from 'react'; -import { isOperation } from '@/features/oss/models/oss-api'; - import { Dropdown } from '@/components/dropdown'; import { type IBlock, type IOperation, type IOssItem } from '../../../../models/oss'; +import { isOperation } from '../../../../models/oss-api'; import { MenuBlock } from './menu-block'; import { MenuOperation } from './menu-operation'; diff --git a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/menu-block.tsx b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/menu-block.tsx index 658ee67a..8b09b2e0 100644 --- a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/menu-block.tsx +++ b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/menu-block.tsx @@ -1,13 +1,13 @@ 'use client'; -import { useDeleteBlock } from '@/features/oss/backend/use-delete-block'; - import { DropdownButton } from '@/components/dropdown'; import { IconDestroy, IconEdit2 } from '@/components/icons'; import { useDialogsStore } from '@/stores/dialogs'; +import { useDeleteBlock } from '../../../../backend/use-delete-block'; import { useMutatingOss } from '../../../../backend/use-mutating-oss'; import { type IBlock } from '../../../../models/oss'; +import { LayoutManager } from '../../../../models/oss-layout-api'; import { useOssEdit } from '../../oss-edit-context'; import { useGetLayout } from '../use-get-layout'; @@ -30,9 +30,8 @@ export function MenuBlock({ block, onHide }: MenuBlockProps) { } onHide(); showEditBlock({ - oss: schema, - target: block, - layout: getLayout() + manager: new LayoutManager(schema, getLayout()), + target: block }); } diff --git a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/menu-operation.tsx b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/menu-operation.tsx index 706d186d..5e5be8c1 100644 --- a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/menu-operation.tsx +++ b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/menu-operation.tsx @@ -3,8 +3,6 @@ import { toast } from 'react-toastify'; import { urls, useConceptNavigation } from '@/app'; import { useLibrary } from '@/features/library/backend/use-library'; -import { useCreateInput } from '@/features/oss/backend/use-create-input'; -import { useExecuteOperation } from '@/features/oss/backend/use-execute-operation'; import { DropdownButton } from '@/components/dropdown'; import { @@ -21,8 +19,11 @@ import { errorMsg } from '@/utils/labels'; import { prepareTooltip } from '@/utils/utils'; import { OperationType } from '../../../../backend/types'; +import { useCreateInput } from '../../../../backend/use-create-input'; +import { useExecuteOperation } from '../../../../backend/use-execute-operation'; import { useMutatingOss } from '../../../../backend/use-mutating-oss'; import { type IOperation } from '../../../../models/oss'; +import { LayoutManager } from '../../../../models/oss-layout-api'; import { useOssEdit } from '../../oss-edit-context'; import { useGetLayout } from '../use-get-layout'; @@ -93,9 +94,8 @@ export function MenuOperation({ operation, onHide }: MenuOperationProps) { } onHide(); showEditOperation({ - oss: schema, - target: operation, - layout: getLayout() + manager: new LayoutManager(schema, getLayout()), + target: operation }); } diff --git a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/graph/block-node.tsx b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/graph/block-node.tsx index 3f589400..231b9851 100644 --- a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/graph/block-node.tsx +++ b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/graph/block-node.tsx @@ -3,13 +3,12 @@ import { NodeResizeControl } from 'reactflow'; import clsx from 'clsx'; -import { useOperationTooltipStore } from '@/features/oss/stores/operation-tooltip'; -import { useOSSGraphStore } from '@/features/oss/stores/oss-graph'; - import { IconResize } from '@/components/icons'; import { globalIDs } from '@/utils/constants'; import { type BlockInternalNode } from '../../../../models/oss-layout'; +import { useOperationTooltipStore } from '../../../../stores/operation-tooltip'; +import { useOSSGraphStore } from '../../../../stores/oss-graph'; import { useOssEdit } from '../../oss-edit-context'; import { useOssFlow } from '../oss-flow-context'; diff --git a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/graph/node-core.tsx b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/graph/node-core.tsx index 906d953a..a88b2667 100644 --- a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/graph/node-core.tsx +++ b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/graph/node-core.tsx @@ -2,8 +2,6 @@ import clsx from 'clsx'; -import { useOSSGraphStore } from '@/features/oss/stores/oss-graph'; - import { IconConsolidation, IconRSForm } from '@/components/icons'; import { cn } from '@/components/utils'; import { Indicator } from '@/components/view'; @@ -12,11 +10,9 @@ import { globalIDs } from '@/utils/constants'; import { OperationType } from '../../../../backend/types'; import { type OperationInternalNode } from '../../../../models/oss-layout'; import { useOperationTooltipStore } from '../../../../stores/operation-tooltip'; +import { useOSSGraphStore } from '../../../../stores/oss-graph'; import { useOssEdit } from '../../oss-edit-context'; -export const OPERATION_NODE_WIDTH = 150; -export const OPERATION_NODE_HEIGHT = 40; - // characters - threshold for long labels - small font const LONG_LABEL_CHARS = 14; diff --git a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/oss-flow.tsx b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/oss-flow.tsx index e5cb81df..e72026b9 100644 --- a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/oss-flow.tsx +++ b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/oss-flow.tsx @@ -13,20 +13,19 @@ import { } from 'reactflow'; import clsx from 'clsx'; -import { useDeleteBlock } from '@/features/oss/backend/use-delete-block'; -import { useMoveItems } from '@/features/oss/backend/use-move-items'; -import { type IOperationSchema } from '@/features/oss/models/oss'; - import { useThrottleCallback } from '@/hooks/use-throttle-callback'; import { useMainHeight } from '@/stores/app-layout'; import { useDialogsStore } from '@/stores/dialogs'; import { PARAMETER } from '@/utils/constants'; import { promptText } from '@/utils/labels'; +import { useDeleteBlock } from '../../../backend/use-delete-block'; +import { useMoveItems } from '../../../backend/use-move-items'; import { useMutatingOss } from '../../../backend/use-mutating-oss'; import { useUpdateLayout } from '../../../backend/use-update-layout'; -import { GRID_SIZE } from '../../../models/oss-api'; +import { type IOperationSchema } from '../../../models/oss'; import { type OssNode, type Position2D } from '../../../models/oss-layout'; +import { GRID_SIZE, LayoutManager } from '../../../models/oss-layout-api'; import { useOperationTooltipStore } from '../../../stores/operation-tooltip'; import { useOSSGraphStore } from '../../../stores/oss-graph'; import { useOssEdit } from '../oss-edit-context'; @@ -153,10 +152,9 @@ export function OssFlow() { function handleCreateOperation() { const targetPosition = screenToFlowPosition({ x: window.innerWidth / 2, y: window.innerHeight / 2 }); showCreateOperation({ - oss: schema, + manager: new LayoutManager(schema, getLayout()), defaultX: targetPosition.x, defaultY: targetPosition.y, - layout: getLayout(), initialInputs: selected.filter(id => id > 0), initialParent: extractSingleBlock(selected), onCreate: () => @@ -167,10 +165,9 @@ export function OssFlow() { function handleCreateBlock() { const targetPosition = screenToFlowPosition({ x: window.innerWidth / 2, y: window.innerHeight / 2 }); showCreateBlock({ - oss: schema, + manager: new LayoutManager(schema, getLayout()), defaultX: targetPosition.x, defaultY: targetPosition.y, - layout: getLayout(), initialInputs: selected, onCreate: () => setTimeout(() => fitView({ duration: PARAMETER.zoomDuration, padding: VIEW_PADDING }), PARAMETER.refreshTimeout) @@ -226,9 +223,8 @@ export function OssFlow() { const block = schema.blockByID.get(-Number(node.id)); if (block) { showEditBlock({ - oss: schema, - target: block, - layout: getLayout() + manager: new LayoutManager(schema, getLayout()), + target: block }); } } else { diff --git a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/toolbar-oss-graph.tsx b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/toolbar-oss-graph.tsx index a5de6d46..342c9b95 100644 --- a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/toolbar-oss-graph.tsx +++ b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/toolbar-oss-graph.tsx @@ -5,8 +5,6 @@ import { useReactFlow } from 'reactflow'; import { HelpTopic } from '@/features/help'; import { BadgeHelp } from '@/features/help/components'; -import { useExecuteOperation } from '@/features/oss/backend/use-execute-operation'; -import { useUpdateLayout } from '@/features/oss/backend/use-update-layout'; import { MiniButton } from '@/components/control'; import { @@ -28,7 +26,10 @@ import { PARAMETER } from '@/utils/constants'; import { prepareTooltip } from '@/utils/utils'; 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 { LayoutManager } from '../../../models/oss-layout-api'; import { useOssEdit } from '../oss-edit-context'; import { VIEW_PADDING } from './oss-flow'; @@ -114,15 +115,13 @@ export function ToolbarOssGraph({ function handleEditItem() { if (selectedOperation) { showEditOperation({ - oss: schema, - target: selectedOperation, - layout: getLayout() + manager: new LayoutManager(schema, getLayout()), + target: selectedOperation }); } else if (selectedBlock) { showEditBlock({ - oss: schema, - target: selectedBlock, - layout: getLayout() + manager: new LayoutManager(schema, getLayout()), + target: selectedBlock }); } } diff --git a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/use-get-layout.tsx b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/use-get-layout.tsx index a765f0d6..ac81cb18 100644 --- a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/use-get-layout.tsx +++ b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/use-get-layout.tsx @@ -1,9 +1,8 @@ import { type Node, useReactFlow } from 'reactflow'; -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 { type IOssLayout } from '../../../backend/types'; +import { type IOperationSchema } from '../../../models/oss'; +import { type Position2D } from '../../../models/oss-layout'; import { useOssEdit } from '../oss-edit-context'; import { BLOCK_NODE_MIN_HEIGHT, BLOCK_NODE_MIN_WIDTH } from './graph/block-node'; diff --git a/rsconcept/frontend/src/features/rsform/pages/rsform-page/editor-rslist/toolbar-rslist.tsx b/rsconcept/frontend/src/features/rsform/pages/rsform-page/editor-rslist/toolbar-rslist.tsx index 8665189c..a42571cb 100644 --- a/rsconcept/frontend/src/features/rsform/pages/rsform-page/editor-rslist/toolbar-rslist.tsx +++ b/rsconcept/frontend/src/features/rsform/pages/rsform-page/editor-rslist/toolbar-rslist.tsx @@ -3,7 +3,6 @@ import clsx from 'clsx'; import { HelpTopic } from '@/features/help'; import { BadgeHelp } from '@/features/help/components'; import { MiniSelectorOSS } from '@/features/library/components'; -import { CstType } from '@/features/rsform'; import { MiniButton } from '@/components/control'; import { Dropdown, DropdownButton, useDropdown } from '@/components/dropdown'; @@ -19,6 +18,7 @@ import { import { prefixes } from '@/utils/constants'; import { prepareTooltip } from '@/utils/utils'; +import { CstType } from '../../../backend/types'; import { useMutatingRSForm } from '../../../backend/use-mutating-rsform'; import { IconCstType } from '../../../components/icon-cst-type'; import { getCstTypeShortcut, labelCstType } from '../../../labels'; diff --git a/rsconcept/frontend/src/features/rsform/pages/rsform-page/editor-term-graph/graph/tg-node.tsx b/rsconcept/frontend/src/features/rsform/pages/rsform-page/editor-term-graph/graph/tg-node.tsx index 1d68cb34..689d97a1 100644 --- a/rsconcept/frontend/src/features/rsform/pages/rsform-page/editor-term-graph/graph/tg-node.tsx +++ b/rsconcept/frontend/src/features/rsform/pages/rsform-page/editor-term-graph/graph/tg-node.tsx @@ -3,12 +3,11 @@ import { Handle, Position } from 'reactflow'; import clsx from 'clsx'; -import { labelCstTypification } from '@/features/rsform/labels'; - import { APP_COLORS } from '@/styling/colors'; import { globalIDs } from '@/utils/constants'; import { colorBgGraphNode } from '../../../../colors'; +import { labelCstTypification } from '../../../../labels'; import { type IConstituenta } from '../../../../models/rsform'; import { useTermGraphStore } from '../../../../stores/term-graph'; import { useRSEdit } from '../../rsedit-context';