diff --git a/src/App.css b/src/App.css index 4072ad0..4e3ca3e 100644 --- a/src/App.css +++ b/src/App.css @@ -31,6 +31,23 @@ border: 1px solid #ccc; } +.toolbarBlocksNewSpiszeno { + width: 70px; + height: 94px; + border: 2px solid #ccc; + backgroundColor: lightgray; + margin: 5px 0; + display: flex; + alignItems: center; + justifyContent: center; + flexDirection: column; + cursor: grab; + fontSize: 12px; + fontWeight: bold; + borderRadius: 4px; + boxShadow: 0 2px 4px rgba(0,0,0,0.1); +} + .tabButtons { display: flex; flex-wrap: wrap; /* перенос строк */ @@ -80,4 +97,179 @@ .dragging { opacity: 0.7; +} + + + + +/* Убедитесь, что блоки стоят выше сетки по событиям */ +.block-container { + pointer-events: all !important; + cursor: grab !important; +} + +/* Сетка НЕ должна перехватывать мышь */ +.grid-line, #background-layer { + pointer-events: none !important; +} + +/* Весь SVG должен пропускать клики до фона, если нет попадания в блок */ +.canvas svg { + pointer-events: none; +} + +/* Но элементы внутри (блоки и соединения) — должны ловить */ +.canvas svg > g { + pointer-events: all; +} + + +/* ✅ Фикс pointer-events */ +.canvas { + position: fixed !important; + /*pointer-events: none !important; /* Контейнер НЕ перехватывает события */ +} + +.canvas svg { + pointer-events: all !important; /* SVG получает ВСЕ события */ +} + +.canvas svg * { + pointer-events: auto; /* Все элементы внутри SVG */ +} + +/* Блоки */ +.block-container { + cursor: grab !important; +} + +.block-container:active { + cursor: grabbing !important; +} + +/* Сетка и фон - НЕ перехватывают drag */ +.grid-line { + pointer-events: none; +} + +/* Фон canvas */ +#background-layer { + pointer-events: none; +} + +.canvas svg g.block-container { + cursor: grab !important; +} + +.canvas svg g.block-container:hover { + filter: drop-shadow(0 0 3px rgba(0,0,255,0.3)); +} + + + + +.canvas svg g.block-container, +.canvas svg .block-container { + pointer-events: all !important; + cursor: grab !important; + z-index: 1000 !important; /* Блоки поверх всего */ +} + +.canvas svg line, +.canvas svg #background-layer * { + pointer-events: none !important; +} + + +/* SVG блоки ловят события! */ +.canvas svg g.block-container { + pointer-events: all !important; +} + +.canvas svg g.block-container * { + pointer-events: auto !important; +} + +/* Фон/линии НЕ ловят */ +.canvas svg line, +.canvas svg .grid-line { + pointer-events: none !important; +} + + + + + + +/*ДЛя блоков: +
+
{label}
+
+
+
+
+
*/ + + + .canvas svg { + /* Чтобы клики по пустому месту пролетали сквозь SVG к div (для пана), + но ловились блоками */ + pointer-events: none; +} + +.canvas svg g.block-container { + pointer-events: all !important; +} + +.canvas svg { + pointer-events: all !important; /* SVG должен быть интерактивным */ + overflow: visible; +} + +.block-container { + pointer-events: all !important; + cursor: move !important; +} + +/* Линии сетки не должны мешать */ +.grid-line { + pointer-events: none !important; +} + +/* Отключаем выделение текста, которое мешает драгу */ +.canvas { + user-select: none; + -webkit-user-select: none; +} + +/* Убеждаемся, что слои под блоками не мешают */ +#background-layer, #connection-layer { + pointer-events: none !important; +} + +.block-container:hover { + outline: 2px solid red !important; +} + +@keyframes pulse { + from { r: 4; stroke: rgba(255, 215, 0, 0.4); stroke-width: 0; } + to { r: 7; stroke: rgba(255, 215, 0, 1); stroke-width: 3; } } \ No newline at end of file diff --git a/src/App.js b/src/App.js index 5960a8b..eec8322 100644 --- a/src/App.js +++ b/src/App.js @@ -1,51 +1,134 @@ -import React, { useState } from 'react'; +import React, { useState, useCallback } from 'react'; import ControlPanel from './components/ControlPanel'; -import Canvas from './components/Canvas'; import Toolbar from './components/Toolbar'; +import Canvas from './components/Canvas'; import './App.css'; + function App() { - const [blocks, setBlocks] = useState([]); //блоки - const [connections, setConnections] = useState([]); //соединения + const [blocks, setBlocks] = useState([]); + const [connections, setConnections] = useState([]); + const [activePort, setActivePort] = useState(null); - const addBlock = (block, x = 100, y = 100) => { //добавить блок на холст - const newBlock = {...block, id: Date.now(), x, y}; - setBlocks(prevBlocks => [...prevBlocks, newBlock]); - }; + const addBlock = useCallback((block, id = Date.now(), x, y) => { + setBlocks(currentBlocks => { + if (currentBlocks.some(block => block.id === id)) + return currentBlocks; + const newBlock = {...block, id, x, y}; + return [...currentBlocks, newBlock]; + }); + }, []); + + const moveBlock = useCallback((id, x, y) => { + setBlocks(currentBlocks => { + const currentBlock = currentBlocks.find(block => block.id === id); + if (currentBlock && currentBlock.x === x && currentBlock.y === y) + return currentBlocks; + return currentBlocks.map(block => + block.id === id ? { ...block, x, y } : block + ); + }); + }, []); + + const removeBlock = useCallback((id) => { + setBlocks(currentBlocks => { + const currentBlock = currentBlocks.find(block => block.id === id); + if (!currentBlock) + return currentBlocks; + return currentBlocks.filter(block => + block.id !== id + ); + }); + }, []); + + + const addConnection = useCallback((from, to, type = 'line') => { + setConnections(currentConnections => [...currentConnections, { from, to, type }]); + }, []); + + const handlePortClick = useCallback((blockId, portType, portIndex) => { + const used = connections.some(connection => { + if (portType === 'input') + return(connection.to.blockId === blockId && connection.to.portIndex === portIndex); + return(connection.from.blockId === blockId && connection.from.portIndex === portIndex); + }); + const toBlock = blocks.find(block => block.id === blockId); + if (used || (portType === 'input' && toBlock.inputCount < portIndex) || (portType === 'output' && toBlock.outputCount < portIndex)) + return; + + if (blockId === activePort?.blockId && portIndex === activePort?.portIndex) { + setActivePort(null); + return; + } + + if (portType === 'output' && activePort?.portType === 'input') { + if (activePort?.blockId !== blockId) { + addConnection({blockId, portType, portIndex}, activePort); + setActivePort(null); + } + else + setActivePort({blockId, portType, portIndex}); + return; + } + + if (portType === 'input' && activePort?.portType === 'output') { + if (activePort.blockId !== blockId) { + const toPort = { + blockId, + portType, + portIndex + } + addConnection(activePort, {blockId, portType, portIndex}); + setActivePort(null); + } + else + setActivePort({blockId, portType, portIndex}); + return; + } + + setActivePort({blockId, portType, portIndex}); + }, [activePort, blocks, connections]); + + + + + + + - const addConnection = (from, to) => { //добавить соединение - setConnections([...connections, {from, to}]); - } -// Функция экспорта: создаёт JSON и скачивает файл const handleExport = () => { - const data = { blocks, connections }; - const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = 'fbd-diagram.json'; - a.click(); - URL.revokeObjectURL(url); }; - // Функция импорта: загружает данные из файла const handleImport = (data) => { - setBlocks(data.blocks || []); - setConnections(data.connections || []); }; - // Функция очистки: сбрасывает холст const handleClear = () => { - setBlocks([]); - setConnections([]); }; - return ( -
- - - + + + + + + return( +
+ + +
); }; diff --git a/src/components/Block.js b/src/components/Block.js index d1eb7ee..1971020 100644 --- a/src/components/Block.js +++ b/src/components/Block.js @@ -1,78 +1,91 @@ -import React from 'react'; +import React, {memo} from 'react'; -const Block = ({ type, label, addConnection, blockId }) => { - const handleOutputClick = () => { - // Сохраняем ID блока как "from" для соединения (в реальности нужно состояние для выбора) - // Для простоты: при клике на выход устанавливаем "from", при клике на вход — "to" - window.selectedFrom = blockId; // глобальная переменная для примера, лучше использовать state - }; +const CENTER_X = 4; +const CENTER_Y = 0; - const handleInputClick = () => { - if (window.selectedFrom && window.selectedFrom != blockId) { - addConnection(window.selectedFrom, blockId); // создание соединения - window.selectedFrom = null; - } - }; +const Block = ({ blockId, type, label, inputCount, outputCount, activePort, onPortClick, connections }) => { - //Условная логика для разных типов блоков - const renderBlock = () => { - const centerX = 4; // width/2 - const centerY = 0; // height/2 + const blockHeight = 50 + (Math.max(inputCount, outputCount, 2) - 1) * 34; + const blockWidth = blockHeight / 1.4; - switch (type) { - case 'NOT': - return ( - - - - {label} + const getPortStyle = (portType, portIndex) => { + const active = activePort?.portType === portType && activePort?.portIndex === portIndex; + const used = connections.some(connection => { + if (portType === 'input') + return(connection.to.blockId === blockId && connection.to.portIndex === portIndex); + return(connection.from.blockId === blockId && connection.from.portIndex === portIndex); + }); + const color = active ? '#999' : + portType === 'input' ? + used ? '#BABAFF' : '#1717D8' : + used ? '#FD90B0' : '#D8000A'; - {/* вход */} - - {/* выход */} - - - - ); - case 'OR': - return ( - - - - {label} + return { + cursor: 'pointer', + pointerEvents: 'auto', + r: active ? 6 : 4, + fill: color + }; + }; - {/* вход */} - - {/* вход 2*/} - - {/* выход */} - - - - ); - default: - return ( - - - - {label} + const getCoord = (count) => { + const coord = []; + if (count === 1) + coord.push(blockHeight / 2); + else { + const step = (blockHeight - 50) / (count - 1); + for (let i = 0; i < count; i++) + coord.push(25 + i * step); + } + return coord; + } - {/* вход */} - - {/* вход 2*/} - - {/* выход */} - - - - ); - } - } + const inputsCoord = getCoord(inputCount); + const outputsCoord = getCoord(outputCount); - return renderBlock(); + const renderBlock = () => ( + + + {label} + + {// + + inputsCoord.map((coord, ind) => ( + { + e.stopPropagation(); + onPortClick(blockId, 'input', ind + 1); + }} + {...getPortStyle('input', ind + 1)} + /> + ))} + { + outputsCoord.map((coord, ind) => ( + { + e.stopPropagation(); + onPortClick(blockId, 'output', ind + 1); + }} + {...getPortStyle('output', ind + 1)} + /> + ))} + + ); + + return renderBlock(); }; -export default Block; +export default memo(Block); /* треугольник diff --git a/src/components/Canvas.js b/src/components/Canvas.js index 8270933..2ebe4a3 100644 --- a/src/components/Canvas.js +++ b/src/components/Canvas.js @@ -1,167 +1,188 @@ -import React, { useRef, useState, useEffect } from 'react'; -import { useDrop } from 'react-dnd'; -import { useDrag } from '@use-gesture/react'; -import Block from './Block'; +import React, { useRef, useState, useEffect, memo } from 'react'; +import { useDrop, useDrag } from 'react-dnd'; import Connection from './Connection'; +import DraggableBlock from './DraggableBlock'; -const Canvas = ({ blocks, setBlocks, connections, addConnection, addBlock }) => { - const safeBlocks = blocks || []; - const safeConnections = connections || []; +const Canvas = ({ blocks = [], connections = [], activePort = null, addConnection, addBlock, moveBlock, removeBlock, onPortClick }) => { + const blocksOnCanvas = blocks || []; + const connectionsOnCanvas = connections || []; - //состояния для маштабирования и перемещения - const [zoom, setZoom] = useState(1); //масштаб 100% - const [pan, setPan] = useState({ x: 0, y: 0 }); //смещение - const [isPanning, setIsPanning] = useState(false); //флаг перемещения - const [panStart, setPanStart] = useState({ x: 0, y: 0}); //начальная точка pan + const [zoom, setZoom] = useState(1); + const [pan, setPan] = useState({x: 0, y: 0}); + const [panStart, setPanStart] = useState({x: 0, y: 0}); + const [isPanning, setIsPanning] = useState(false); + const canvasRef = useRef(null); - const [canvasSize, setCanvasSize] = useState({ width: 800, height: 500 }); - const canvasRef = useRef (null); + const [{isOver, monitor}, drop] = useDrop(() => ({ + accept: ['block', 'newBlock'], + hover: (item, monitor) => { + const offset = monitor.getClientOffset(); + if (!offset || !canvasRef.current) + return; + const svgRect = canvasRef.current.getBoundingClientRect(); - useEffect(() => { - const updateSize = () => { - if (canvasRef.current) { - const rect = canvasRef.current.getBoundingClientRect(); - setCanvasSize({ width: rect.width, height: rect.height }); - } - }; - updateSize(); //первичное обновление - window.addEventListener('resize', updateSize); //обновление при изменении размера окна - return () => window.removeEventListener('resize', updateSize); //очистка - }, []); + //координаты мыши + const mouseOnCanvasX = offset.x - svgRect.x; + const mouseOnCanvasY = offset.y - svgRect.y; - const [{ isOver }, drop] = useDrop(() => ({ - accept: 'block', - drop: (item, monitor) => { - const offset = monitor.getClientOffset(); //позиция курсора - //console.log('Offset: ', offset); - if (offset && canvasRef.current) { - const canvasRect = canvasRef.current.getBoundingClientRect(); // границы svg - //console.log('CanvasRect: ', canvasRect); - const x = offset.x - canvasRect.left; - const y = offset.y - canvasRect.top; - //console.log('Calculated x/y: ', x, y); + //координаты на canvas + const mouseX = (mouseOnCanvasX - pan.x) / zoom; + const mouseY = (mouseOnCanvasY - pan.y) / zoom; - if (item.id) { - console.log('moveBlock'); - //перемещение - moveBlock(item.id, x, y); - } else { - console.log('addBlock'); - //добавление нового - addBlock(item, x, y); - } - } - }, - collect: (monitor) => ({ - isOver: monitor.isOver(), //курсор с блоком над canvas - }), - })); + //смещение + const blockWidth = 68; + const blockHeight = 84; + const x = mouseX - blockWidth / 3; + const y = mouseY - blockHeight / 3; - const moveBlock = (id, x, y) => { - setBlocks(safeBlocks.map(block => block.id === id ? { ...block, x, y} : block)); - } + if (item.id && !item.isNew) + moveBlock(item.id, x, y); + else { + if (!item.tempId) { + const newId = Date.now(); + item.tempId = newId; + item.isNew = false; + addBlock(item, newId, x, y); + } + else + moveBlock(item.tempId, x, y); + } + }, + drop: (item) => { item.tempId = null; }, + collect: (monitor) => ({ + isOver: monitor.isOver(), + monitor: monitor, + }), + }), [moveBlock, addBlock, pan, zoom]); - // обработчик масштабирования - const handleWheel = (e) => { - e.preventDefault(); - const stepZoom = 0.1; - const newZoom = zoom + (e.deltaY > 0 ? -stepZoom : stepZoom); - setZoom (Math.max(0.7, Math.min(2, newZoom))); - }; + useEffect(() => { + //удаление блока при выходе за пределы холста + const draggingItem = monitor?.getItem(); + if (!isOver && draggingItem?.tempId) { + removeBlock(draggingItem.tempId); + draggingItem.tempId = null; + } + }, [isOver, monitor, removeBlock]); + const handleWheel = (e) => { + e.preventDefault(); + const rect = canvasRef.current.getBoundingClientRect(); + const mouseOnCanvasX = e.clientX - rect.x; + const mouseOnCanvasY = e.clientY - rect.y; + const mouseX = (mouseOnCanvasX - pan.x) / zoom; + const mouseY = (mouseOnCanvasY - pan.y) / zoom; - //обработчик начала перемещения - const handleMouseDown = (e) => { - if (e.button === 0) { //лкм - setIsPanning(true); - setPanStart({ x: e.clientX - pan.x, y: e.clientY - pan.y }); - } - }; + const stepZoom = 0.075; + const prewZoom = zoom + (e.deltaY > 0 ? -stepZoom : stepZoom); + const newZoom = Math.max(0.6, Math.min(1.7, prewZoom)); - //обработчик перемещения - const handleMouseMove = (e) => { - if (isPanning) { - setPan ({ - x: e.clientX - panStart.x, - y: e.clientY - panStart.y, - }); - } - }; + const newPanX = mouseOnCanvasX - mouseX * newZoom; + const newPanY = mouseOnCanvasY - mouseY * newZoom; - //обработчик окончания перемещения - const handleMouseUp = () => { - setIsPanning(false); - } + setZoom(newZoom); + setPan({x: newPanX, y: newPanY}); + }; - //сетка - const renderGrid = () => { - const gridSize = 7 * zoom; //размер ячейки - const numLinesX = Math.ceil(canvasSize.width / gridSize) + 10; - const numLinesY = Math.ceil(canvasSize.height / gridSize) + 10; - - const lines = []; + const handleMouseDown = (e) => { + if (e.target.closest('.block-container') || e.target.closest('circle')) + return; + if (e.button === 0) { + setIsPanning(true); + setPanStart({ + x: e.clientX - pan.x, + y: e.clientY - pan.y + }); + } + }; - for (let i = 0; i < numLinesX; i++) { - lines.push(); - } - for (let i = 0; i < numLinesY; i++) { - lines.push(); - } //` - return lines; - }; + const handleMouseMove = (e) => { + if (isPanning) + setPan({ + x: e.clientX - panStart.x, + y: e.clientY - panStart.y + }); + }; + const handleMouseUp = () => { setIsPanning(false); }; - return ( -
- - {renderGrid()} - {/* Перебираем соединения и рендерим каждое */} - {safeConnections.map((connect, index) => ( - - ))} - {/* Перебираем блоки и рендерим каждый блок в SVG */} - {safeBlocks.map((block) => ( - - ))} - -
- ); + useEffect(() => { + const handleGlobalMouseUp = () => setIsPanning(false); + window.addEventListener('mouseup', handleGlobalMouseUp); + return() => window.removeEventListener('mouseup', handleGlobalMouseUp); + }, []); + + return( +
{ + if (node) { + canvasRef.current = node; + drop(node); + } + }} + className = "canvas" + onWheel = {handleWheel} + onMouseDown = {handleMouseDown} + onMouseMove = {handleMouseMove} + onMouseUp = {handleMouseUp} + > + + + {/* Сетка */} + + + + + {// + } + + + + + + + + + {/* Блоки и соединения */} + + + {connectionsOnCanvas.map((connection, index) => ( + + ))} + + + {// + } + + {blocksOnCanvas.map((block) => ( + + ))} + + + +
+ ); }; -const DraggableBlock = ({ block, moveBlock, addConnection }) => { - const [isDragging, setIsDragging] = useState(false); // Состояние для drag - - - const bind = useDrag(({ active, movement: [mx, my] }) => { - setIsDragging(active); // active = true во время drag - if (!active) { - // При отпускании обновляем позицию - const newX = block.x + mx; - const newY = block.y + my; - moveBlock(block.id, newX, newY); - } - }); - - return ( - - - - ); -}; - -export default Canvas; \ No newline at end of file +export default memo(Canvas); \ No newline at end of file diff --git a/src/components/Connection.js b/src/components/Connection.js index 8d731b7..d64b164 100644 --- a/src/components/Connection.js +++ b/src/components/Connection.js @@ -1,21 +1,32 @@ import React from 'react'; -const Connection = ({ connection, blocks }) => { - //находим блоки по id - const fromBlock = blocks.find (block => block.id === connection.from); - const toBlock = blocks.find (block => block.id === connection.to); +const Connection = ({connection, blocks}) => { + const fromBlock = blocks.find(block => block.id === connection.from.blockId); + const toBlock = blocks.find(block => block.id === connection.to.blockId); + if (!fromBlock || !toBlock) return null; - if (!fromBlock || !toBlock) return null; - //координаты - const xFrom = fromBlock.x + 100; - const yFrom = fromBlock.y + 25; - const xTo = toBlock.x; - const y2 = toBlock.y + 25; + const fromBlockHeight = 50 + (Math.max(fromBlock.inputCount, fromBlock.outputCount, 2) - 1) * 34; + const toBlockHeight = 50 + (Math.max(toBlock.inputCount, toBlock.outputCount, 2) - 1) * 34; + const fromBlockWidth = fromBlockHeight / 1.4 + 3; + const stepFrom = fromBlock.outputCount === 1 ? (fromBlockHeight - 50) / 2 : (fromBlockHeight - 50) / (fromBlock.outputCount - 1); + const stepTo = toBlock.inputCount === 1 ? (toBlockHeight - 50) / 2 : (toBlockHeight - 50) / (toBlock.inputCount - 1); - return ( - - ); + const xFrom = fromBlock.x + fromBlockWidth; + const yFrom = fromBlock.y + 25 + (fromBlock.outputCount === 1 ? stepFrom : (stepFrom * (connection.from.portIndex - 1))); + const xTo = toBlock.x + 2; + const yTo = toBlock.y + 25 + (toBlock.inputCount === 1 ? stepTo : (stepTo * (connection.to.portIndex - 1))); + + return ( + + + + + + ); }; -export default Connection; \ No newline at end of file +export default Connection; + +// последние 2 элемента ещё сырые и их надо будет дорабатывать. +// Connection.js: \ No newline at end of file diff --git a/src/components/DraggableBlock.js b/src/components/DraggableBlock.js new file mode 100644 index 0000000..321dbdd --- /dev/null +++ b/src/components/DraggableBlock.js @@ -0,0 +1,90 @@ +import React, {memo} from 'react'; +import { useDrag } from 'react-dnd'; +import { useEffect } from 'react'; +import { getEmptyImage } from 'react-dnd-html5-backend'; +import Block from './Block'; +import ToolbarBlock from './ToolbarBlock'; + +const DraggableBlock = ({ block, connections, addConnection, activePort, onPortClick, isNew = true }) => { + const blockId = block.id; + const blockType = block.type; + const blockLabel = block.label; + const blockInputCount = block.inputCount; + const blockOutputCount = block.outputCount; + + const [{isDragging}, drag, preview] = useDrag(() => ({ + type: isNew ? 'newBlock' : 'block', + item: isNew + ? { type: blockType, label: blockLabel, inputCount: blockInputCount, outputCount: blockOutputCount } + : { id: blockId, type: blockType, label: blockLabel, inputCount: blockInputCount, outputCount: blockOutputCount }, + collect: (monitor) => ({ isDragging: monitor.isDragging() }) + }), [blockType, blockLabel, blockId, blockInputCount, blockOutputCount, isNew]); + + useEffect(() => { + preview(getEmptyImage(), {captureDraggingState: true}); + }, [preview]); + + const draggingStyle = { + opacity: isDragging ? 0.6 : 1, + cursor: 'move', + pointerEvents: 'all' + }; + + if (isNew) { + return( +
+ {// + } + +
+ // + ); + } + + return( + +
+ +
+
+ ); +}; + +export default memo(DraggableBlock); \ No newline at end of file diff --git a/src/components/Toolbar.js b/src/components/Toolbar.js index 9f0935c..d75b2b4 100644 --- a/src/components/Toolbar.js +++ b/src/components/Toolbar.js @@ -1,106 +1,83 @@ -import React, { useState } from 'react'; -import { useDrag } from 'react-dnd'; -import Block from './Block'; -import Connection from './Connection'; +import React, { useState, useMemo } from 'react'; +import DraggableBlock from './DraggableBlock'; -const Toolbar = ({ onAddBlock }) => { - //состояние для активной вкладки - const [activeTab, setActiveTab] = useState('logical'); - - const blockCategories = { - logical: [ - { type: 'AND', label: 'AND' }, - { type: 'OR', label: 'OR' }, - { type: 'NOT', label: 'NOT' }, - { type: 'XOR', label: 'XOR' } ], - arithmetic: [ - { type: 'ADD', label: 'ADD' }, - { type: 'SUB', label: 'SUB' }, - { type: 'MUL', label: 'MUL' }, - { type: 'DIV', label: 'DIV' } ], - timers: [ - { type: 'TON', label: 'TON' }, - { type: 'TOF', label: 'TOF' }, - { type: 'TP', label: 'TP' } ], - counters: [ - { type: 'CTU', label: 'CTU' }, - { type: 'CTD', label: 'CTD' }, - { type: 'CTUD', label: 'CTUD' } ], - comparisons: [ - { type: 'GT', label: 'GT' }, - { type: 'LT', label: 'LT' }, - { type: 'EQ', label: 'EQ' } ], - other: [ - { type: 'SR', label: 'SR' }, - { type: 'RS', label: 'RS' }, - { type: 'MOVE', label: 'MOVE' } ], - customized: [] - }; - - const activeBlocks = blockCategories[activeTab] || []; - - return ( -
-

Блоки

- -
- {/*//Вкладки*/} -
- {Object.keys (blockCategories).map((tab) => ( - - ))} -
- - {/*Блоки*/} -
- {activeBlocks.map((block) => ( - - ))} -
-
-
- ); +const NAME_CATEGORIES = { + logical: 'Логические', + arithmetic: 'Арифметические', + timers: 'Таймеры', + counters: 'Счётчики', + comparisons: 'Сравнения', + other: 'Другие', + customized: 'Пользовательские' }; -const DraggableBlock = ({ block, onAddBlock }) => { - const [{ isDragging }, drag] = useDrag(() => ({ - type: 'block', - item: block, - end: (item, monitor) => { - if (monitor.didDrop()) {} - }, - collect: (monitor) => ({ //состояние - isDragging: monitor.isDragging(), - }), - })); +const BLOCK_CATEGORIES = { + logical: [ + { type: 'and', label: 'AND', inputCount: 2, outputCount: 1 }, + { type: 'or', label: 'OR', inputCount: 2, outputCount: 1 }, + { type: 'not', label: 'NOT', inputCount: 1, outputCount: 1 }, + { type: 'xor', label: 'XOR', inputCount: 2, outputCount: 1 }], + arithmetic: [ + { type: 'add', label: 'ADD', inputCount: 2, outputCount: 1 }, + { type: 'sub', label: 'SUB', inputCount: 2, outputCount: 1 }, + { type: 'mul', label: 'MUL', inputCount: 2, outputCount: 1 }, + { type: 'div', label: 'DIV', inputCount: 2, outputCount: 1 }], + timers: [ + { type: 'ton', label: 'TON', inputCount: 2, outputCount: 1 }, + { type: 'tof', label: 'TOF', inputCount: 2, outputCount: 1 }, + { type: 'tp', label: 'TP', inputCount: 2, outputCount: 1 }], + counters: [ + { type: 'ctu', label: 'CTU', inputCount: 2, outputCount: 1 }, + { type: 'ctd', label: 'CTD', inputCount: 2, outputCount: 1 }, + { type: 'ctud', label: 'CTUD',inputCount: 2, outputCount: 1 }], + comparisons: [ + { type: 'gt', label: 'GT', inputCount: 2, outputCount: 1 }, + { type: 'lt', label: 'LT', inputCount: 2, outputCount: 1 }, + { type: 'eq', label: 'EQ', inputCount: 2, outputCount: 1 }], + other: [ + { type: 'sr', label: 'SR', inputCount: 2, outputCount: 1 }, + { type: 'rs', label: 'RS', inputCount: 2, outputCount: 1 }, + { type: 'move', label: 'MOVE', inputCount: 2, outputCount: 1 }], + customized: [] +}; - return ( -
- -
- ); + +const Toolbar = () => { + const [activeTab, setActiveTab] = useState('logical'); + const tabs = useMemo(() => Object.keys(BLOCK_CATEGORIES), []); + const activeBlocks = BLOCK_CATEGORIES[activeTab] || []; + + return( +
+

Блоки

+
+ { //Вкладки + } +
+ {tabs.map((tab) => ( + + ))} +
+ + { /*Блоки*/ } +
+ {activeBlocks.map((block) => ( + + ))} +
+
+
+ ); }; export default Toolbar; \ No newline at end of file diff --git a/src/components/ToolbarBlock.js b/src/components/ToolbarBlock.js new file mode 100644 index 0000000..def1aaa --- /dev/null +++ b/src/components/ToolbarBlock.js @@ -0,0 +1,59 @@ +import React, {memo} from 'react'; + +const CENTER_X = 4; +const CENTER_Y = 0; + +const ToolbarBlock = ({ type, label, inputCount, outputCount }) => { + + const blockHeight = 50 + (Math.max(inputCount, outputCount, 2) - 1) * 34; + const blockWidth = blockHeight / 1.4; + + const renderBlock = () => { + const textX = type === 'or' ? 32 : 24; + const outputsCoord = []; + const inputsCoord = []; + + if (inputCount === 1) + inputsCoord.push(blockHeight / 2); + else { + const stepInput = (blockHeight - 50) / (inputCount - 1); + for (let i = 0; i < inputCount; i++) + inputsCoord.push(25 + i * stepInput); + } + + if (outputCount === 1) + outputsCoord.push(blockHeight / 2); + else { + const stepOutput = (blockHeight - 50) / (outputCount - 1); + for (let i = 0; i < outputCount; i++) + outputsCoord.push(25 + i * stepOutput); + } + + return( + + + {label} + + {// + inputsCoord.map((coord, ind) => ( + + ))} + {outputsCoord.map((coord, ind) => ( + + ))} + + ); + }; + + return(renderBlock()); +}; + +export default memo(ToolbarBlock); \ No newline at end of file