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