fbd-editor/src/components/Canvas.js
2026-05-15 08:00:32 +03:00

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);