Compare commits
No commits in common. "b89c963cdcef560049422e8c2d9536e0ed38811c" and "4f27f5ff61955cb3d89bff219a0aa4f1983ae43a" have entirely different histories.
b89c963cdc
...
4f27f5ff61
@ -68,3 +68,4 @@ This section has moved here: [https://facebook.github.io/create-react-app/docs/d
|
|||||||
### `npm run build` fails to minify
|
### `npm run build` fails to minify
|
||||||
|
|
||||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
||||||
|
"# fbd-editor"
|
||||||
|
|||||||
192
src/App.css
192
src/App.css
@ -31,6 +31,23 @@
|
|||||||
border: 1px solid #ccc;
|
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 {
|
.tabButtons {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap; /* перенос строк */
|
flex-wrap: wrap; /* перенос строк */
|
||||||
@ -81,3 +98,178 @@
|
|||||||
.dragging {
|
.dragging {
|
||||||
opacity: 0.7;
|
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; }
|
||||||
|
}
|
||||||
127
src/App.js
127
src/App.js
@ -1,23 +1,134 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import ControlPanel from './components/ControlPanel';
|
import ControlPanel from './components/ControlPanel';
|
||||||
import Canvas from './components/Canvas';
|
|
||||||
import Toolbar from './components/Toolbar';
|
import Toolbar from './components/Toolbar';
|
||||||
|
import Canvas from './components/Canvas';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [blocks, setBlocks] = useState([]);
|
const [blocks, setBlocks] = useState([]);
|
||||||
|
const [connections, setConnections] = useState([]);
|
||||||
|
const [activePort, setActivePort] = useState(null);
|
||||||
|
|
||||||
const addBlock = (block, x = 100, y = 100) => { //добавить блок на холст
|
const addBlock = useCallback((block, id = Date.now(), x, y) => {
|
||||||
const newBlock = {...block, id: Date.now(), x, y};
|
setBlocks(currentBlocks => {
|
||||||
setBlocks(prevBlocks => [...prevBlocks, newBlock]);
|
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 handleExport = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleImport = (data) => {
|
||||||
|
};
|
||||||
|
const handleClear = () => {
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return(
|
return(
|
||||||
<div className = "App">
|
<div className = "App">
|
||||||
<ControlPanel />
|
<ControlPanel
|
||||||
<Toolbar onAddBlock={addBlock} />
|
onExport = {handleExport}
|
||||||
<Canvas blocks={blocks} setBlocks={setBlocks} addBlock = {addBlock} />
|
onImport = {handleImport}
|
||||||
|
onClear = {handleClear}
|
||||||
|
/>
|
||||||
|
<Toolbar/>
|
||||||
|
<Canvas
|
||||||
|
blocks = {blocks}
|
||||||
|
addBlock = {addBlock}
|
||||||
|
moveBlock = {moveBlock}
|
||||||
|
removeBlock = {removeBlock}
|
||||||
|
connections = {connections}
|
||||||
|
addConnection = {addConnection}
|
||||||
|
activePort = {activePort}
|
||||||
|
onPortClick = {handlePortClick}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,62 +1,92 @@
|
|||||||
import React from 'react';
|
import React, {memo} from 'react';
|
||||||
|
|
||||||
const Block = ({ type, label, blockId }) => {
|
const CENTER_X = 4;
|
||||||
|
const CENTER_Y = 0;
|
||||||
|
|
||||||
const renderBlock = () => {
|
const Block = ({ blockId, type, label, inputCount, outputCount, activePort, onPortClick, connections }) => {
|
||||||
const centerX = 4;
|
|
||||||
const centerY = 0;
|
|
||||||
|
|
||||||
switch (type) {
|
const blockHeight = 50 + (Math.max(inputCount, outputCount, 2) - 1) * 34;
|
||||||
case 'NOT':
|
const blockWidth = blockHeight / 1.4;
|
||||||
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) => {
|
||||||
<circle cx = "0" cy = "42" r = "4" fill = "#1A1ADB" style = {{ cursor: 'pointer' }} />
|
const active = activePort?.portType === portType && activePort?.portIndex === portIndex;
|
||||||
{/* выход */}
|
const used = connections.some(connection => {
|
||||||
<circle cx = "60" cy = "42" r = "4" fill = "#E00010" style = {{ cursor: 'pointer' }} />
|
if (portType === 'input')
|
||||||
</g>
|
return(connection.to.blockId === blockId && connection.to.portIndex === portIndex);
|
||||||
</svg>
|
return(connection.from.blockId === blockId && connection.from.portIndex === portIndex);
|
||||||
);
|
});
|
||||||
case 'OR':
|
const color = active ? '#999' :
|
||||||
return (
|
portType === 'input' ?
|
||||||
<svg width = "68" height = "84" alight = "center">
|
used ? '#BABAFF' : '#1717D8' :
|
||||||
<g transform={`translate(${centerX}, ${centerY})`}>
|
used ? '#FD90B0' : '#D8000A';
|
||||||
<rect width="60" height = "100%" fill = "lightgray" stroke = "black" />
|
|
||||||
<text x = "32" y = "20" textAnchor = "left" fontSize = "14" style={{ pointerEvents: 'none' }}>{label}</text>
|
|
||||||
|
|
||||||
{/* вход */}
|
return {
|
||||||
<circle cx = "0" cy = "25" r = "4" fill = "#1A1ADB" style = {{ cursor: 'pointer' }} />
|
cursor: 'pointer',
|
||||||
{/* вход 2*/}
|
pointerEvents: 'auto',
|
||||||
<circle cx = "0" cy = "59" r = "4" fill = "#1A1ADB" style = {{ cursor: 'pointer' }} />
|
r: active ? 6 : 4,
|
||||||
{/* выход */}
|
fill: color
|
||||||
<circle cx = "60" cy = "42" r = "4" fill = "#E00010" 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) => {
|
||||||
<circle cx = "0" cy = "25" r = "4" fill = "#1A1ADB" style = {{ cursor: 'pointer' }} />
|
const coord = [];
|
||||||
{/* вход 2*/}
|
if (count === 1)
|
||||||
<circle cx = "0" cy = "59" r = "4" fill = "#1A1ADB" style = {{ cursor: 'pointer' }} />
|
coord.push(blockHeight / 2);
|
||||||
{/* выход */}
|
else {
|
||||||
<circle cx = "60" cy = "42" r = "4" fill = "#E00010" style = {{ cursor: 'pointer' }} />
|
const step = (blockHeight - 50) / (count - 1);
|
||||||
</g>
|
for (let i = 0; i < count; i++)
|
||||||
</svg>
|
coord.push(25 + i * step);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
return coord;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const inputsCoord = getCoord(inputCount);
|
||||||
|
const outputsCoord = getCoord(outputCount);
|
||||||
|
|
||||||
|
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();
|
return renderBlock();
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Block;
|
export default memo(Block);
|
||||||
|
|
||||||
|
|
||||||
|
/* треугольник
|
||||||
|
<polygon points="60,35 60,49 70,42" fill = "none" stroke="black" />*/
|
||||||
@ -1,105 +1,188 @@
|
|||||||
import React, { useRef, useState, useEffect } from 'react';
|
import React, { useRef, useState, useEffect, memo } from 'react';
|
||||||
import { useDrop } from 'react-dnd';
|
import { useDrop, useDrag } from 'react-dnd';
|
||||||
import { useDrag } from '@use-gesture/react';
|
import Connection from './Connection';
|
||||||
import Block from './Block';
|
import DraggableBlock from './DraggableBlock';
|
||||||
|
|
||||||
const Canvas = ({ blocks, setBlocks, addBlock }) => {
|
const Canvas = ({ blocks = [], connections = [], activePort = null, addConnection, addBlock, moveBlock, removeBlock, onPortClick }) => {
|
||||||
const safeBlocks = blocks || [];
|
const blocksOnCanvas = blocks || [];
|
||||||
|
const connectionsOnCanvas = connections || [];
|
||||||
|
|
||||||
const [canvasSize, setCanvasSize] = useState({ width: 800, height: 500 });
|
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 canvasRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
const [{isOver, monitor}, drop] = useDrop(() => ({
|
||||||
const updateSize = () => {
|
accept: ['block', 'newBlock'],
|
||||||
if (canvasRef.current) {
|
hover: (item, monitor) => {
|
||||||
const rect = canvasRef.current.getBoundingClientRect();
|
const offset = monitor.getClientOffset();
|
||||||
setCanvasSize({ width: rect.width, height: rect.height });
|
if (!offset || !canvasRef.current)
|
||||||
}
|
return;
|
||||||
};
|
const svgRect = canvasRef.current.getBoundingClientRect();
|
||||||
updateSize(); //первичное обновление
|
|
||||||
window.addEventListener('resize', updateSize); //обновление при изменении размера окна
|
|
||||||
return () => window.removeEventListener('resize', updateSize); //очистка
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const [{ isOver }, drop] = useDrop(() => ({
|
//координаты мыши
|
||||||
accept: 'block',
|
const mouseOnCanvasX = offset.x - svgRect.x;
|
||||||
drop: (item, monitor) => {
|
const mouseOnCanvasY = offset.y - svgRect.y;
|
||||||
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);
|
|
||||||
|
|
||||||
if (item.id) {
|
//координаты на canvas
|
||||||
console.log('moveBlock');
|
const mouseX = (mouseOnCanvasX - pan.x) / zoom;
|
||||||
//перемещение
|
const mouseY = (mouseOnCanvasY - pan.y) / zoom;
|
||||||
|
|
||||||
|
//смещение
|
||||||
|
const blockWidth = 68;
|
||||||
|
const blockHeight = 84;
|
||||||
|
const x = mouseX - blockWidth / 3;
|
||||||
|
const y = mouseY - blockHeight / 3;
|
||||||
|
|
||||||
|
if (item.id && !item.isNew)
|
||||||
moveBlock(item.id, x, y);
|
moveBlock(item.id, x, y);
|
||||||
} else {
|
else {
|
||||||
console.log('addBlock');
|
if (!item.tempId) {
|
||||||
//добавление нового
|
const newId = Date.now();
|
||||||
addBlock(item, x, y);
|
item.tempId = newId;
|
||||||
|
item.isNew = false;
|
||||||
|
addBlock(item, newId, x, y);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
moveBlock(item.tempId, x, y);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
drop: (item) => { item.tempId = null; },
|
||||||
collect: (monitor) => ({
|
collect: (monitor) => ({
|
||||||
isOver: monitor.isOver(), //курсор с блоком над canvas
|
isOver: monitor.isOver(),
|
||||||
|
monitor: monitor,
|
||||||
}),
|
}),
|
||||||
}));
|
}), [moveBlock, addBlock, pan, zoom]);
|
||||||
|
|
||||||
const moveBlock = (id, x, y) => {
|
useEffect(() => {
|
||||||
setBlocks(safeBlocks.map(block => block.id === id ? { ...block, x, y} : block));
|
//удаление блока при выходе за пределы холста
|
||||||
|
const draggingItem = monitor?.getItem();
|
||||||
|
if (!isOver && draggingItem?.tempId) {
|
||||||
|
removeBlock(draggingItem.tempId);
|
||||||
|
draggingItem.tempId = null;
|
||||||
}
|
}
|
||||||
|
}, [isOver, monitor, removeBlock]);
|
||||||
|
|
||||||
//сетка
|
const handleWheel = (e) => {
|
||||||
const renderGrid = () => {
|
e.preventDefault();
|
||||||
const gridSize = 7;
|
const rect = canvasRef.current.getBoundingClientRect();
|
||||||
const numLinesX = Math.ceil(canvasSize.width / gridSize) + 10;
|
const mouseOnCanvasX = e.clientX - rect.x;
|
||||||
const numLinesY = Math.ceil(canvasSize.height / gridSize) + 10;
|
const mouseOnCanvasY = e.clientY - rect.y;
|
||||||
|
const mouseX = (mouseOnCanvasX - pan.x) / zoom;
|
||||||
|
const mouseY = (mouseOnCanvasY - pan.y) / zoom;
|
||||||
|
|
||||||
const lines = [];
|
const stepZoom = 0.075;
|
||||||
|
const prewZoom = zoom + (e.deltaY > 0 ? -stepZoom : stepZoom);
|
||||||
|
const newZoom = Math.max(0.6, Math.min(1.7, prewZoom));
|
||||||
|
|
||||||
for (let i = 0; i < numLinesX; i++) {
|
const newPanX = mouseOnCanvasX - mouseX * newZoom;
|
||||||
lines.push(<line key={`v${i}`} x1={i * gridSize} y1="0" x2={i * gridSize} y2={canvasSize.height} stroke="#eee" strokeWidth="1" />);
|
const newPanY = mouseOnCanvasY - mouseY * newZoom;
|
||||||
}
|
|
||||||
for (let i = 0; i < numLinesY; i++) {
|
setZoom(newZoom);
|
||||||
lines.push(<line key={`h${i}`} x1="0" y1={i * gridSize} x2={canvasSize.width} y2={i * gridSize} stroke="#eee" strokeWidth="1" />);
|
setPan({x: newPanX, y: newPanY});
|
||||||
} //`
|
|
||||||
return 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseMove = (e) => {
|
||||||
|
if (isPanning)
|
||||||
|
setPan({
|
||||||
|
x: e.clientX - panStart.x,
|
||||||
|
y: e.clientY - panStart.y
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => { setIsPanning(false); };
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleGlobalMouseUp = () => setIsPanning(false);
|
||||||
|
window.addEventListener('mouseup', handleGlobalMouseUp);
|
||||||
|
return() => window.removeEventListener('mouseup', handleGlobalMouseUp);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return(
|
return(
|
||||||
<div ref = {drop} className="canvas" >
|
<div
|
||||||
<svg width = '100vw' height = '100vh' ref = {canvasRef} >
|
ref = {(node) => {
|
||||||
{renderGrid()}
|
if (node) {
|
||||||
{safeBlocks.map((block) => (
|
canvasRef.current = node;
|
||||||
<DraggableBlock key = {block.id} block = {block} moveBlock = {moveBlock} />
|
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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const DraggableBlock = ({ block, moveBlock }) => {
|
export default memo(Canvas);
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
|
||||||
|
|
||||||
const bind = useDrag(({ active, movement: [mx, my] }) => {
|
|
||||||
setIsDragging(active);
|
|
||||||
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} blockId={block.id} />
|
|
||||||
</g>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Canvas;
|
|
||||||
32
src/components/Connection.js
Normal file
32
src/components/Connection.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// последние 2 элемента ещё сырые и их надо будет дорабатывать.
|
||||||
|
// Connection.js:
|
||||||
@ -1,11 +1,29 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
const ControlPanel = () => {
|
const ControlPanel = ({ onExport, onImport, onClear }) => {
|
||||||
|
// Обработчик импорта файла (читает JSON и передаёт в App)
|
||||||
|
const handleImport = (event) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(e.target.result); // Парсим JSON
|
||||||
|
onImport(data); // Вызываем функцию из App
|
||||||
|
} catch (error) {
|
||||||
|
alert('Ошибка при чтении файла: ' + error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="controlPanel" style={{ padding: '10px', borderBottom: '1px solid #ccc', backgroundColor: '#f0f0f0', display: 'flex', gap: '10px' }}>
|
<div className="controlPanel" style={{ padding: '10px', borderBottom: '1px solid #ccc', backgroundColor: '#f0f0f0', display: 'flex', gap: '10px' }}>
|
||||||
<button>Экспорт в JSON</button>
|
<button onClick={onExport}>Экспорт в JSON</button>
|
||||||
<button>Очистить холст</button>
|
<input type="file" accept=".json" onChange={handleImport} /> {/* Поле для выбора файла импорта */}
|
||||||
|
<button onClick={onClear}>Очистить холст</button>
|
||||||
|
{/* Можно добавить: <button>Зум +</button> <button>Зум -</button> */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
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,104 +1,82 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
import { useDrag } from 'react-dnd';
|
import DraggableBlock from './DraggableBlock';
|
||||||
import Block from './Block';
|
|
||||||
|
|
||||||
const Toolbar = ({ onAddBlock }) => {
|
const NAME_CATEGORIES = {
|
||||||
//состояние для активной вкладки
|
logical: 'Логические',
|
||||||
const [activeTab, setActiveTab] = useState('logical');
|
arithmetic: 'Арифметические',
|
||||||
|
timers: 'Таймеры',
|
||||||
|
counters: 'Счётчики',
|
||||||
|
comparisons: 'Сравнения',
|
||||||
|
other: 'Другие',
|
||||||
|
customized: 'Пользовательские'
|
||||||
|
};
|
||||||
|
|
||||||
const blockCategories = {
|
const BLOCK_CATEGORIES = {
|
||||||
logical: [
|
logical: [
|
||||||
{ type: 'AND', label: 'AND' },
|
{ type: 'and', label: 'AND', inputCount: 2, outputCount: 1 },
|
||||||
{ type: 'OR', label: 'OR' },
|
{ type: 'or', label: 'OR', inputCount: 2, outputCount: 1 },
|
||||||
{ type: 'NOT', label: 'NOT' },
|
{ type: 'not', label: 'NOT', inputCount: 1, outputCount: 1 },
|
||||||
{ type: 'XOR', label: 'XOR' } ],
|
{ type: 'xor', label: 'XOR', inputCount: 2, outputCount: 1 }],
|
||||||
arithmetic: [
|
arithmetic: [
|
||||||
{ type: 'ADD', label: 'ADD' },
|
{ type: 'add', label: 'ADD', inputCount: 2, outputCount: 1 },
|
||||||
{ type: 'SUB', label: 'SUB' },
|
{ type: 'sub', label: 'SUB', inputCount: 2, outputCount: 1 },
|
||||||
{ type: 'MUL', label: 'MUL' },
|
{ type: 'mul', label: 'MUL', inputCount: 2, outputCount: 1 },
|
||||||
{ type: 'DIV', label: 'DIV' } ],
|
{ type: 'div', label: 'DIV', inputCount: 2, outputCount: 1 }],
|
||||||
timers: [
|
timers: [
|
||||||
{ type: 'TON', label: 'TON' },
|
{ type: 'ton', label: 'TON', inputCount: 2, outputCount: 1 },
|
||||||
{ type: 'TOF', label: 'TOF' },
|
{ type: 'tof', label: 'TOF', inputCount: 2, outputCount: 1 },
|
||||||
{ type: 'TP', label: 'TP' } ],
|
{ type: 'tp', label: 'TP', inputCount: 2, outputCount: 1 }],
|
||||||
counters: [
|
counters: [
|
||||||
{ type: 'CTU', label: 'CTU' },
|
{ type: 'ctu', label: 'CTU', inputCount: 2, outputCount: 1 },
|
||||||
{ type: 'CTD', label: 'CTD' },
|
{ type: 'ctd', label: 'CTD', inputCount: 2, outputCount: 1 },
|
||||||
{ type: 'CTUD', label: 'CTUD' } ],
|
{ type: 'ctud', label: 'CTUD',inputCount: 2, outputCount: 1 }],
|
||||||
comparisons: [
|
comparisons: [
|
||||||
{ type: 'GT', label: 'GT' },
|
{ type: 'gt', label: 'GT', inputCount: 2, outputCount: 1 },
|
||||||
{ type: 'LT', label: 'LT' },
|
{ type: 'lt', label: 'LT', inputCount: 2, outputCount: 1 },
|
||||||
{ type: 'EQ', label: 'EQ' } ],
|
{ type: 'eq', label: 'EQ', inputCount: 2, outputCount: 1 }],
|
||||||
other: [
|
other: [
|
||||||
{ type: 'SR', label: 'SR' },
|
{ type: 'sr', label: 'SR', inputCount: 2, outputCount: 1 },
|
||||||
{ type: 'RS', label: 'RS' },
|
{ type: 'rs', label: 'RS', inputCount: 2, outputCount: 1 },
|
||||||
{ type: 'MOVE', label: 'MOVE' } ],
|
{ type: 'move', label: 'MOVE', inputCount: 2, outputCount: 1 }],
|
||||||
customized: []
|
customized: []
|
||||||
};
|
};
|
||||||
|
|
||||||
const activeBlocks = blockCategories[activeTab] || [];
|
|
||||||
|
const Toolbar = () => {
|
||||||
|
const [activeTab, setActiveTab] = useState('logical');
|
||||||
|
const tabs = useMemo(() => Object.keys(BLOCK_CATEGORIES), []);
|
||||||
|
const activeBlocks = BLOCK_CATEGORIES[activeTab] || [];
|
||||||
|
|
||||||
return(
|
return(
|
||||||
<div className = "toolbar">
|
<div className = "toolbar">
|
||||||
<h4 style = {{ marginTop: '3px', marginBottom: '15px' }}>Блоки</h4>
|
<h4 style = {{ marginTop: '3px', marginBottom: '15px' }}>Блоки</h4>
|
||||||
|
|
||||||
<div className = "toolbarElements">
|
<div className = "toolbarElements">
|
||||||
{/*//Вкладки*/}
|
{ //Вкладки
|
||||||
|
}
|
||||||
<div className = "tabButtons">
|
<div className = "tabButtons">
|
||||||
{Object.keys (blockCategories).map((tab) => (
|
{tabs.map((tab) => (
|
||||||
<button
|
<button
|
||||||
key = {tab}
|
key = {tab}
|
||||||
className = {activeTab === tab ? 'activeTab' : ''}
|
className = { activeTab === tab ? 'activeTab' : 'notActiveTab' }
|
||||||
data-row = { tab === 'customized' || tab === 'other' || tab === 'comparisons' ? '2' : '1' }
|
data-row = { tab === 'customized' || tab === 'other' || tab === 'comparisons' ? '2' : '1' }
|
||||||
onClick = {() => setActiveTab(tab)}
|
onClick = {() => setActiveTab(tab)}
|
||||||
title = {
|
title = {NAME_CATEGORIES[tab]}
|
||||||
tab === 'logical' ? 'Логические' :
|
> { NAME_CATEGORIES[tab] } </button>
|
||||||
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>
|
||||||
|
|
||||||
{ /*Блоки*/ }
|
{ /*Блоки*/ }
|
||||||
<div className = "toolbarBlocks">
|
<div className = "toolbarBlocks">
|
||||||
{activeBlocks.map((block) => (
|
{activeBlocks.map((block) => (
|
||||||
<DraggableBlock key = {block.type} block = {block} onAddBlock = {onAddBlock} />
|
<DraggableBlock
|
||||||
|
key = {block.type}
|
||||||
|
block = {block}
|
||||||
|
isNew = {true}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
); {/*//Блоки*/}
|
|
||||||
};
|
|
||||||
|
|
||||||
const DraggableBlock = ({ block, onAddBlock }) => {
|
|
||||||
const [{ isDragging }, drag] = useDrag(() => ({
|
|
||||||
type: 'block',
|
|
||||||
item: block,
|
|
||||||
end: (item, monitor) => {
|
|
||||||
if (monitor.didDrop()) {}
|
|
||||||
},
|
|
||||||
collect: (monitor) => ({ //состояние
|
|
||||||
isDragging: monitor.isDragging(),
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={drag} style={{ opacity: isDragging ? 0.7 : 1 }}>
|
|
||||||
<Block type={block.type} label={block.label} />
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
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