188 lines
5.6 KiB
JavaScript
188 lines
5.6 KiB
JavaScript
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 = [], connections = [], activePort = null, addConnection, addBlock, moveBlock, removeBlock, onPortClick }) => {
|
|
const blocksOnCanvas = blocks || [];
|
|
const connectionsOnCanvas = connections || [];
|
|
|
|
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 [{isOver, monitor}, drop] = useDrop(() => ({
|
|
accept: ['block', 'newBlock'],
|
|
hover: (item, monitor) => {
|
|
const offset = monitor.getClientOffset();
|
|
if (!offset || !canvasRef.current)
|
|
return;
|
|
const svgRect = canvasRef.current.getBoundingClientRect();
|
|
|
|
//координаты мыши
|
|
const mouseOnCanvasX = offset.x - svgRect.x;
|
|
const mouseOnCanvasY = offset.y - svgRect.y;
|
|
|
|
//координаты на canvas
|
|
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);
|
|
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]);
|
|
|
|
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 stepZoom = 0.075;
|
|
const prewZoom = zoom + (e.deltaY > 0 ? -stepZoom : stepZoom);
|
|
const newZoom = Math.max(0.6, Math.min(1.7, prewZoom));
|
|
|
|
const newPanX = mouseOnCanvasX - mouseX * newZoom;
|
|
const newPanY = mouseOnCanvasY - mouseY * newZoom;
|
|
|
|
setZoom(newZoom);
|
|
setPan({x: newPanX, y: newPanY});
|
|
};
|
|
|
|
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(
|
|
<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>
|
|
);
|
|
};
|
|
|
|
export default memo(Canvas); |