изменение интерфейса
This commit is contained in:
parent
d1c289ec68
commit
4f27f5ff61
192
src/App.css
192
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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/*ДЛя блоков:
|
||||
<div
|
||||
style={{
|
||||
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)',
|
||||
...getBlockStyle()
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: '5px' }}>{label}</div>
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||
<div style={{ width: '8px', height: '8px', backgroundColor: '#1A1ADB', borderRadius: '50%' }}></div>
|
||||
<div style={{ width: '8px', height: '8px', backgroundColor: '#E00010', borderRadius: '50%' }}></div>
|
||||
</div>
|
||||
</div>*/
|
||||
|
||||
|
||||
.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; }
|
||||
}
|
||||
145
src/App.js
145
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 (
|
||||
<div className="App">
|
||||
<ControlPanel onExport = {handleExport} onImport = {handleImport} onClear = {handleClear} />
|
||||
<Toolbar onAddBlock={addBlock} />
|
||||
<Canvas blocks={blocks} setBlocks={setBlocks} connections = {connections} addConnection = {addConnection} addBlock = {addBlock} />
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return(
|
||||
<div className = "App">
|
||||
<ControlPanel
|
||||
onExport = {handleExport}
|
||||
onImport = {handleImport}
|
||||
onClear = {handleClear}
|
||||
/>
|
||||
<Toolbar/>
|
||||
<Canvas
|
||||
blocks = {blocks}
|
||||
addBlock = {addBlock}
|
||||
moveBlock = {moveBlock}
|
||||
removeBlock = {removeBlock}
|
||||
connections = {connections}
|
||||
addConnection = {addConnection}
|
||||
activePort = {activePort}
|
||||
onPortClick = {handlePortClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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 (
|
||||
<svg width = "68" height = "84" alight = "center">
|
||||
<g transform={`translate(${centerX}, ${centerY})`}>
|
||||
<rect width="60" height = "100%" fill = "lightgray" stroke = "black" />
|
||||
<text x = "24" y = "20" textAnchor = "left" fontSize = "14" style={{ pointerEvents: 'none' }}>{label}</text>
|
||||
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';
|
||||
|
||||
{/* вход */}
|
||||
<circle cx = "0" cy = "42" r = "4" fill = "#1A1ADB" onClick = {handleInputClick} style = {{ cursor: 'pointer' }} />
|
||||
{/* выход */}
|
||||
<circle cx = "60" cy = "42" r = "4" fill = "#E00010" onClick = {handleOutputClick} style = {{ cursor: 'pointer' }} />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
case 'OR':
|
||||
return (
|
||||
<svg width = "68" height = "84" alight = "center">
|
||||
<g transform={`translate(${centerX}, ${centerY})`}>
|
||||
<rect width="60" height = "100%" fill = "lightgray" stroke = "black" />
|
||||
<text x = "32" y = "20" textAnchor = "left" fontSize = "14" style={{ pointerEvents: 'none' }}>{label}</text>
|
||||
return {
|
||||
cursor: 'pointer',
|
||||
pointerEvents: 'auto',
|
||||
r: active ? 6 : 4,
|
||||
fill: color
|
||||
};
|
||||
};
|
||||
|
||||
{/* вход */}
|
||||
<circle cx = "0" cy = "25" r = "4" fill = "#1A1ADB" onClick = {handleInputClick} style = {{ cursor: 'pointer' }} />
|
||||
{/* вход 2*/}
|
||||
<circle cx = "0" cy = "59" r = "4" fill = "#1A1ADB" onClick = {handleInputClick} style = {{ cursor: 'pointer' }} />
|
||||
{/* выход */}
|
||||
<circle cx = "60" cy = "42" r = "4" fill = "#E00010" onClick = {handleOutputClick} style = {{ cursor: 'pointer' }} />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<svg width = "68" height = "84" alight = "center">
|
||||
<g transform={`translate(${centerX}, ${centerY})`}>
|
||||
<rect width="60" height = "100%" fill = "lightgray" stroke = "black" />
|
||||
<text x = "24" y = "20" textAnchor = "left" fontSize = "14" style={{ pointerEvents: 'none' }}>{label}</text>
|
||||
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;
|
||||
}
|
||||
|
||||
{/* вход */}
|
||||
<circle cx = "0" cy = "25" r = "4" fill = "#1A1ADB" onClick = {handleInputClick} style = {{ cursor: 'pointer' }} />
|
||||
{/* вход 2*/}
|
||||
<circle cx = "0" cy = "59" r = "4" fill = "#1A1ADB" onClick = {handleInputClick} style = {{ cursor: 'pointer' }} />
|
||||
{/* выход */}
|
||||
<circle cx = "60" cy = "42" r = "4" fill = "#E00010" onClick = {handleOutputClick} style = {{ cursor: 'pointer' }} />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
}
|
||||
const inputsCoord = getCoord(inputCount);
|
||||
const outputsCoord = getCoord(outputCount);
|
||||
|
||||
return renderBlock();
|
||||
const renderBlock = () => (
|
||||
<svg
|
||||
width={blockWidth + 2 * CENTER_X}
|
||||
height={blockHeight}
|
||||
viewBox={`0 0 ${blockWidth + 2 * CENTER_X} ${blockHeight}`}
|
||||
style={{
|
||||
overflow: 'visible',
|
||||
transform: `translate(${CENTER_X}px, ${CENTER_Y}px)`
|
||||
}}
|
||||
>
|
||||
<rect width = {blockWidth} height = {blockHeight} fill = "lightgray" stroke = "black"/>
|
||||
<text x = {type === 'or' ? 32 : 24} y = "20" textAnchor = "start" fontSize = "14" style = {{pointerEvents: 'none'}}>{label}</text>
|
||||
|
||||
{//
|
||||
|
||||
inputsCoord.map((coord, ind) => (
|
||||
<circle key = {`b-${blockId}-in${ind}`}
|
||||
cx = "0" cy = {coord}
|
||||
onClick = {(e) => {
|
||||
e.stopPropagation();
|
||||
onPortClick(blockId, 'input', ind + 1);
|
||||
}}
|
||||
{...getPortStyle('input', ind + 1)}
|
||||
/>
|
||||
))}
|
||||
{
|
||||
outputsCoord.map((coord, ind) => (
|
||||
<circle key = {`b-${blockId}-out${ind}`}
|
||||
cx = {blockWidth} cy = {coord}
|
||||
onClick = {(e) => {
|
||||
e.stopPropagation();
|
||||
onPortClick(blockId, 'output', ind + 1);
|
||||
}}
|
||||
{...getPortStyle('output', ind + 1)}
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
);
|
||||
|
||||
return renderBlock();
|
||||
};
|
||||
|
||||
export default Block;
|
||||
export default memo(Block);
|
||||
|
||||
|
||||
/* треугольник
|
||||
|
||||
@ -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(<line key={`v${i}`} x1={i * gridSize} y1="0" x2={i * gridSize} y2={canvasSize.height} stroke="#eee" strokeWidth="1" />);
|
||||
}
|
||||
for (let i = 0; i < numLinesY; i++) {
|
||||
lines.push(<line key={`h${i}`} x1="0" y1={i * gridSize} x2={canvasSize.width} y2={i * gridSize} stroke="#eee" strokeWidth="1" />);
|
||||
} //`
|
||||
return lines;
|
||||
};
|
||||
const handleMouseMove = (e) => {
|
||||
if (isPanning)
|
||||
setPan({
|
||||
x: e.clientX - panStart.x,
|
||||
y: e.clientY - panStart.y
|
||||
});
|
||||
};
|
||||
|
||||
const handleMouseUp = () => { setIsPanning(false); };
|
||||
|
||||
return (
|
||||
<div
|
||||
ref = {drop}
|
||||
className="canvas"
|
||||
onWheel = {handleWheel} // маштабирование
|
||||
onMouseDown = {handleMouseDown} // начало перемещения
|
||||
onMouseMove = {handleMouseMove} // перемещение
|
||||
onMouseUp = {handleMouseUp} // конец перемещения
|
||||
>
|
||||
<svg
|
||||
width = '100vw'
|
||||
height = '100vh'
|
||||
ref = {canvasRef}
|
||||
style = {{
|
||||
transform: `translate(${pan.x}px, ${pan.y}px) scale(${zoom}))`,
|
||||
transformOrigin: '0 0',
|
||||
}}
|
||||
>
|
||||
{renderGrid()}
|
||||
{/* Перебираем соединения и рендерим каждое */}
|
||||
{safeConnections.map((connect, index) => (
|
||||
<Connection key = {index} connection = {connect} blocks = {safeBlocks} />
|
||||
))}
|
||||
{/* Перебираем блоки и рендерим каждый блок в SVG */}
|
||||
{safeBlocks.map((block) => (
|
||||
<DraggableBlock key = {block.id} block = {block} moveBlock = {moveBlock} addConnection = {addConnection} />
|
||||
))}
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
useEffect(() => {
|
||||
const handleGlobalMouseUp = () => setIsPanning(false);
|
||||
window.addEventListener('mouseup', handleGlobalMouseUp);
|
||||
return() => window.removeEventListener('mouseup', handleGlobalMouseUp);
|
||||
}, []);
|
||||
|
||||
return(
|
||||
<div
|
||||
ref = {(node) => {
|
||||
if (node) {
|
||||
canvasRef.current = node;
|
||||
drop(node);
|
||||
}
|
||||
}}
|
||||
className = "canvas"
|
||||
onWheel = {handleWheel}
|
||||
onMouseDown = {handleMouseDown}
|
||||
onMouseMove = {handleMouseMove}
|
||||
onMouseUp = {handleMouseUp}
|
||||
>
|
||||
<svg width = "100%" height = "100%">
|
||||
|
||||
{/* Сетка */}
|
||||
<defs>
|
||||
<pattern id="grid" width={8} height={8} patternUnits="userSpaceOnUse">
|
||||
<path d="M 8 0 L 0 0 0 8" fill="none" stroke="#eee" strokeWidth="1" />
|
||||
</pattern>
|
||||
{//
|
||||
}
|
||||
<pattern
|
||||
id="main-grid"
|
||||
width={80} height={80}
|
||||
patternUnits="userSpaceOnUse"
|
||||
patternTransform={`translate(${pan.x}, ${pan.y}) scale(${zoom})`}
|
||||
>
|
||||
<rect width={80} height={80} fill="url(#grid)" />
|
||||
<path d="M 80 0 L 0 0 0 80" fill="none" stroke="#ddd" strokeWidth="1.25" />
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<rect
|
||||
id = "background-layer"
|
||||
width="100%"
|
||||
height="100%"
|
||||
fill="url(#main-grid)"
|
||||
/>
|
||||
|
||||
{/* Блоки и соединения */}
|
||||
<g style = {{
|
||||
transform: `translate(${pan.x}px, ${pan.y}px) scale(${zoom})`,
|
||||
transformOrigin: '0 0',
|
||||
pointerEvents: 'all'
|
||||
}}>
|
||||
<g id = "connection-layer">
|
||||
{connectionsOnCanvas.map((connection, index) => (
|
||||
<Connection key = {index} connection = {connection} blocks = {blocksOnCanvas} />
|
||||
))}
|
||||
</g>
|
||||
|
||||
{//
|
||||
}
|
||||
<g id = "block-layer">
|
||||
{blocksOnCanvas.map((block) => (
|
||||
<DraggableBlock
|
||||
key = {block.id}
|
||||
block = {block}
|
||||
addConnection = {addConnection}
|
||||
onPortClick = {onPortClick}
|
||||
activePort = {activePort?.blockId === block.id ? activePort : null}
|
||||
connections = {connectionsOnCanvas}
|
||||
isNew = {false}
|
||||
/>
|
||||
))}
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<g {...bind()} transform={`translate(${block.x}, ${block.y})`} className={isDragging ? 'dragging' : ''} style={{ cursor: 'move' }}>
|
||||
<Block type={block.type} label={block.label} addConnection={addConnection} blockId={block.id} />
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
export default Canvas;
|
||||
export default memo(Canvas);
|
||||
@ -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 (
|
||||
<line xFrom = {xFrom} yFrom = {yFrom} xTo = {xTo} yTo = {yTo} stroke = "black" strokeWidth = "2" />
|
||||
);
|
||||
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 (
|
||||
<g data-connection-from={connection.from} data-connection-to={connection.to}>
|
||||
<line x1 = {xFrom} y1 = {yFrom} x2 = {xFrom + 15} y2 = {yFrom} stroke = "black" strokeWidth = "2" />
|
||||
<line x1 = {xFrom + 15} y1 = {yFrom} x2 = {xTo - 15} y2 = {yTo} stroke = "black" strokeWidth = "2" />
|
||||
<line x1 = {xTo} y1 = {yTo} x2 = {xTo - 15} y2 = {yTo} stroke = "black" strokeWidth = "2" />
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
export default Connection;
|
||||
export default Connection;
|
||||
|
||||
// последние 2 элемента ещё сырые и их надо будет дорабатывать.
|
||||
// Connection.js:
|
||||
90
src/components/DraggableBlock.js
Normal file
90
src/components/DraggableBlock.js
Normal file
@ -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(
|
||||
<div
|
||||
ref = {drag}
|
||||
style = {{
|
||||
...draggingStyle,
|
||||
width: `68px`,
|
||||
height: `84px`,
|
||||
margin: '5px auto'
|
||||
}}
|
||||
>
|
||||
{// <ToolbarBlock type={block.type} label={block.label} />
|
||||
}
|
||||
<ToolbarBlock
|
||||
type = {block.type}
|
||||
label = {block.label}
|
||||
inputCount = {block.inputCount}
|
||||
outputCount = {block.outputCount}
|
||||
/>
|
||||
</div>
|
||||
//
|
||||
);
|
||||
}
|
||||
|
||||
return(
|
||||
<foreignObject
|
||||
className = "block-container"
|
||||
x = {block.x || 0}
|
||||
y = {block.y || 0}
|
||||
width = "68"
|
||||
height = "84"
|
||||
style = {{overflow: 'visible'}}
|
||||
>
|
||||
<div
|
||||
ref = {drag}
|
||||
style = {{
|
||||
cursor: 'move',
|
||||
pointerEvents: 'all',
|
||||
opacity: isDragging ? 0.7 : 1
|
||||
}}
|
||||
>
|
||||
<Block
|
||||
key = {block.id}
|
||||
blockId = {block.id}
|
||||
type = {block.type}
|
||||
label = {block.label}
|
||||
inputCount = {block.inputCount}
|
||||
outputCount = {block.outputCount}
|
||||
onPortClick = {onPortClick}
|
||||
activePort = {activePort}
|
||||
connections = {connections}
|
||||
/>
|
||||
</div>
|
||||
</foreignObject>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(DraggableBlock);
|
||||
@ -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 (
|
||||
<div className = "toolbar">
|
||||
<h4 style = {{ marginTop: '3px', marginBottom: '15px' }}>Блоки</h4>
|
||||
|
||||
<div className="toolbarElements">
|
||||
{/*//Вкладки*/}
|
||||
<div className = "tabButtons">
|
||||
{Object.keys (blockCategories).map((tab) => (
|
||||
<button
|
||||
key = {tab}
|
||||
className = {activeTab === tab ? 'activeTab' : ''}
|
||||
data-row = { tab === 'customized' || tab === 'other' || tab === 'comparisons' ? '2' : '1' }
|
||||
onClick = { () => setActiveTab(tab) }
|
||||
title = {
|
||||
tab === 'logical' ? 'Логические' :
|
||||
tab === 'arithmetic' ? 'Арифметические' :
|
||||
tab === 'timers' ? 'Таймеры' :
|
||||
tab === 'counters' ? 'Счётчики' :
|
||||
tab === 'comparisons' ? 'Сравнения' :
|
||||
tab === 'other' ? 'Другие' :
|
||||
'Пользовательские'
|
||||
}
|
||||
> {
|
||||
tab === 'logical' ? 'Логические' :
|
||||
tab === 'arithmetic' ? 'Арифметические' :
|
||||
tab === 'timers' ? 'Таймеры' :
|
||||
tab === 'counters' ? 'Счётчики' :
|
||||
tab === 'comparisons' ? 'Сравнения' :
|
||||
tab === 'other' ? 'Другие' :
|
||||
'Пользовательские'
|
||||
} </button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/*Блоки*/}
|
||||
<div className = "toolbarBlocks">
|
||||
{activeBlocks.map((block) => (
|
||||
<DraggableBlock key = {block.type} block = {block} onAddBlock = {onAddBlock} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
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 (
|
||||
<div ref={drag} style={{ opacity: isDragging ? 0.7 : 1 }}>
|
||||
<Block type={block.type} label={block.label} />
|
||||
</div>
|
||||
);
|
||||
|
||||
const Toolbar = () => {
|
||||
const [activeTab, setActiveTab] = useState('logical');
|
||||
const tabs = useMemo(() => Object.keys(BLOCK_CATEGORIES), []);
|
||||
const activeBlocks = BLOCK_CATEGORIES[activeTab] || [];
|
||||
|
||||
return(
|
||||
<div className = "toolbar">
|
||||
<h4 style = {{ marginTop: '3px', marginBottom: '15px' }}>Блоки</h4>
|
||||
<div className = "toolbarElements">
|
||||
{ //Вкладки
|
||||
}
|
||||
<div className = "tabButtons">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key = {tab}
|
||||
className = { activeTab === tab ? 'activeTab' : 'notActiveTab' }
|
||||
data-row = { tab === 'customized' || tab === 'other' || tab === 'comparisons' ? '2' : '1' }
|
||||
onClick = {() => setActiveTab(tab)}
|
||||
title = {NAME_CATEGORIES[tab]}
|
||||
> { NAME_CATEGORIES[tab] } </button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{ /*Блоки*/ }
|
||||
<div className = "toolbarBlocks">
|
||||
{activeBlocks.map((block) => (
|
||||
<DraggableBlock
|
||||
key = {block.type}
|
||||
block = {block}
|
||||
isNew = {true}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Toolbar;
|
||||
59
src/components/ToolbarBlock.js
Normal file
59
src/components/ToolbarBlock.js
Normal file
@ -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(
|
||||
<svg
|
||||
width={blockWidth + 2 * CENTER_X}
|
||||
height={blockHeight}
|
||||
viewBox={`0 0 ${blockWidth + 2 * CENTER_X} ${blockHeight}`}
|
||||
style={{
|
||||
overflow: 'visible',
|
||||
transform: `translate(${CENTER_X}px, ${CENTER_Y}px)`
|
||||
}}
|
||||
>
|
||||
<rect width = {blockWidth} height = {blockHeight} fill = "lightgray" stroke = "black"/>
|
||||
<text x = {textX} y = "20" textAnchor = "start" fontSize = "14" style = {{pointerEvents: 'none'}}>{label}</text>
|
||||
|
||||
{//
|
||||
inputsCoord.map((coord, ind) => (
|
||||
<circle key = {`in-${ind + 1}`} cx = "0" cy = {coord} r = "4" fill = '#1717D8'/>
|
||||
))}
|
||||
{outputsCoord.map((coord, ind) => (
|
||||
<circle key = {`out-${ind + 1}`} cx = {blockWidth} cy = {coord} r = "4" fill = '#D8000A'/>
|
||||
))}
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
return(renderBlock());
|
||||
};
|
||||
|
||||
export default memo(ToolbarBlock);
|
||||
Loading…
Reference in New Issue
Block a user