Добавлена валидация (циклы)

This commit is contained in:
Ellina Sohnenko 2026-05-24 07:07:27 +03:00
parent 99f520a672
commit fffca3af9a
6 changed files with 596 additions and 104 deletions

View File

@ -97,7 +97,7 @@ html, body, #root {
.toolbarBlocks {
width: 100%;
max-height: calc((100% - 58px) / 3 * 2);
max-height: calc(100% - 58px);
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
@ -122,12 +122,28 @@ html, body, #root {
}
.custom-fbd-block .block-title {
padding-top: 6px;
font-size: 14px;
box-sizing: border-box;
font-size: 12px;
text-align: right;
padding-top: 10px;
padding-right: 7px;
pointer-events: none;
}
.custom-fbd-block .block-inputs-title {
box-sizing: border-box;
font-size: 10px;
text-align: left;
pointer-events: none;
}
.custom-fbd-block .block-outputs-title {
box-sizing: border-box;
font-size: 10px;
text-align: right;
pointer-events: none;
}
.toolbarLines svg {
background-color: white;
@ -188,9 +204,25 @@ html, body, #root {
}
.toolbar-block .preview-title {
box-sizing: border-box;
font-size: 12px;
text-align: left;
text-align: right;
padding-top: 10px;
padding-right: 7px;
pointer-events: none;
}
.toolbar-block .preview-inputs-title {
box-sizing: border-box;
font-size: 10px;
text-align: left;
pointer-events: none;
}
.toolbar-block .preview-outputs-title {
box-sizing: border-box;
font-size: 10px;
text-align: right;
pointer-events: none;
}
@ -234,4 +266,27 @@ html, body, #root {
.clear-button:hover {
background-color: #a80007;
}
.modalDataBack {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modalDataMain {
background: white;
padding: 20px;
border-radius: 8px;
width: 320px;
text-align: left;
font-family: Arial, sans-serif;
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
}

View File

@ -6,6 +6,7 @@ import './App.css'
import Toolbar from './components/Toolbar';
import ControlPanel from './components/ControlPanel';
import CustomBlock from './components/CustomBlock';
import ModalData from './components/ModalData'
const nodeTypes = {
fbdBlock: CustomBlock
@ -13,29 +14,164 @@ const nodeTypes = {
const EDGE_STYLES = {
bool: { stroke: '#000', strokeWidth: 2 },
a: { stroke: '#000', strokeWidth: 2, strokeDasharray: '4 4' }, // Пунктир
b: { stroke: '#D8000A', strokeWidth: 2 }, // Красная
c: { stroke: '#000', strokeWidth: 2, strokeDasharray: '1 3' }, // Точечная
d: { stroke: '#008000', strokeWidth: 2 }, // Зеленая
e: { stroke: '#1717D8', strokeWidth: 2 } // Синяя
};
int: { stroke: '#040089', strokeWidth: 3 },
dint: { stroke: '#771061', strokeWidth: 3 },
real: { stroke: '#EB3B13', strokeWidth: 3 },
time: { stroke: '#AB7001', strokeWidth: 2.5 },
string: { stroke: '#58E9D0', strokeWidth: 2.5 }
}
function App() {
const [nodes, setNodes, onNodesChange] = useNodesState([])
const [edges, setEdges, onEdgesChange] = useEdgesState([])
const [flowInstance, setFlowInstance] = useState(null)
const [edgeType, setEdgeType] = useState('bool')
const [modalData, setModalData] = useState(null)
const onConnect = useCallback(
(params) => setEdges((edge) => {
const currentStyle = EDGE_STYLES[edgeType] || EDGE_STYLES['bool']
return addEdge({
...params,
(params) => {
const sourceNode = nodes.find(node => node.id === params.source)
const sourceIndex = parseInt(params.sourceHandle.split('-')[1], 10)
const sourceType = sourceNode?.data?.outputs[sourceIndex].replace('$/', '') || 'INT'
const newEdge = {
...params,
id: `e_${Date.now()}`,
type: 'smoothstep',
style: currentStyle,
data: {type: edgeType}
}, edge)
}), [setEdges, edgeType]
style: EDGE_STYLES[sourceType.toLowerCase().split('/')[0]] || EDGE_STYLES['bool'],
data: { type: edgeType }
}
const allEdges = [...edges, newEdge]
const { currentNodes, currentEdges } = updateGraphTypes(allEdges)
setNodes(currentNodes)
setEdges(currentEdges)
function checkEdge(edge, currentNodes) {
const sourceNode = currentNodes.find(node => node.id === edge.source)
const targetNode = currentNodes.find(node => node.id === edge.target)
if (!sourceNode || !targetNode) return null
const sourceIndex = parseInt(edge.sourceHandle.split('-')[1], 10)
const targetIndex = parseInt(edge.targetHandle.split('-')[1], 10)
const sourceType = sourceNode.data.outputs[sourceIndex].replace('$/', '') || 'INT'
const targetType = targetNode.data.inputs[targetIndex].replace('$/', '') || 'INT'
if (sourceType === targetType)
return null
let generalType = ''
for (let t of sourceType.split('/'))
if (targetType.includes(t))
generalType += t + '/'
if (generalType.endsWith('/'))
generalType = generalType.slice(0, -1)
if (!generalType)
generalType = sourceType.split('/')[0]
return generalType
}
function updateTwoNode(edge, newType, currentNodes, currentEdges) {
const sourceIndex = parseInt(edge.sourceHandle.split('-')[1], 10)
const targetIndex = parseInt(edge.targetHandle.split('-')[1], 10)
let change = false
const nextNodes = currentNodes.map(node => {
if (node.id === edge.source) {
let newInputs = [...node.data.inputs]
let newOutputs = [...node.data.outputs]
if (node.data.outputs[sourceIndex]?.startsWith('$')) {
for (let ind = 0; ind < newInputs.length; ind++)
if (newInputs[ind].startsWith('$') && ('$/' + newType) !== newInputs[ind]) {
newInputs[ind] = '$/' + newType
change = true
}
for (let ind = 0; ind < newOutputs.length; ind++)
if (newOutputs[ind].startsWith('$') && ('$/' + newType) !== newOutputs[ind]) {
newOutputs[ind] = '$/' + newType
change = true
}
}
else
newOutputs[sourceIndex] = newType
return {
...node,
data: {
...node.data,
inputs: newInputs,
outputs: newOutputs
}
}
}
else if (node.id === edge.target) {
let newInputs = [...node.data.inputs]
let newOutputs = [...node.data.outputs]
if (node.data.inputs[targetIndex]?.startsWith('$')) {
for (let ind = 0; ind < newInputs.length; ind++)
if (newInputs[ind].startsWith('$') && ('$/' + newType) !== newInputs[ind]) {
newInputs[ind] = '$/' + newType
change = true
}
for (let ind = 0; ind < newOutputs.length; ind++)
if (newOutputs[ind].startsWith('$') && ('$/' + newType) !== newOutputs[ind]) {
newOutputs[ind] = '$/' + newType
change = true
}
}
else
newInputs[targetIndex] = newType
return {
...node,
data: {
...node.data,
inputs: newInputs,
outputs: newOutputs
}
}
}
return node
})
const nextEdges = currentEdges.map(ed => {
if (edge.id === ed.id) {
const currentStyle = EDGE_STYLES[newType.toLowerCase().split('/')[0]] || EDGE_STYLES['bool']
return {
...ed,
style: currentStyle
}
}
return ed
})
return {nextNodes, nextEdges, change}
}
function updateGraphTypes(currentEdges) {
let globalChange = true
let currentNodes = [...nodes]
while (globalChange) {
globalChange = false
for (const edge of currentEdges) {
const newType = checkEdge(edge, currentNodes)
if (newType) {
const { nextNodes, nextEdges, change } = updateTwoNode(edge, newType, currentNodes, currentEdges)
currentNodes = nextNodes
currentEdges = nextEdges
globalChange = globalChange || change
}
}
}
return {currentNodes, currentEdges}
}
}, [nodes, setNodes, setEdges, edgeType]
)
const onDragOver = useCallback((event) => {
@ -53,22 +189,116 @@ function App() {
if (!dataStr)
return
const {type, label, inputCount, outputCount} = JSON.parse(dataStr)
const {type, label, inputs = [], outputs = [], offsetX = 0, offsetY = 0} = JSON.parse(dataStr)
const position = flowInstance.screenToFlowPosition({
x: event.clientX,
y: event.clientY
x: event.clientX - offsetX,
y: event.clientY - offsetY
})
if (['const', 'switch_type', 'input', 'output'].includes(type)) {
setModalData({ type, label, position, inputs, outputs })
return
}
const newNode = {
id: `block_${Date.now()}`,
type: 'fbdBlock',
position,
data: {type, label, inputCount, outputCount}
data: { type, label, inputs, outputs }
}
setNodes((currentNodes) => [...currentNodes, newNode])
}, [flowInstance, setNodes]
)
function handleSaveModalData(formData) {
if (!modalData)
return
let finalLabel = modalData.label
let inputs = []
let outputs = []
if (modalData.type === 'const') {
finalLabel = `${formData.value} (${formData.fromType})`
outputs = [formData.fromType]
}
else if (modalData.type === 'switch_type') {
finalLabel = `${formData.fromType}_TO_${formData.toType}`
inputs = [formData.fromType]
outputs = [formData.toType]
}
else if (modalData.type === 'input') {
finalLabel = `${formData.value} (${formData.fromType})`
outputs = [formData.fromType]
}
else if (modalData.type === 'output') {
finalLabel = `${formData.value} (${formData.fromType})`
inputs = [formData.fromType]
}
const newNode = {
id: `block_${Date.now()}`,
type: 'fbdBlock',
position: modalData.position,
data: {
type: modalData.type,
label: finalLabel,
value: formData.value,
inputs,
outputs,
metaFromType: formData.fromType,
metaToType: formData.toType
}
}
setNodes((currentNodes) => [...currentNodes, newNode])
setModalData(null)
}
const isValidConnection = useCallback((connection) => {
if (connection.source === connection.target)
return false
function checkIfCycleExists(sourceId, targetId) {
const visited = new Set()
function dfs(currentId) {
if (currentId === sourceId)
return true
if (visited.has(currentId))
return false
visited.add(currentId)
const outgoingEdges = edges.filter(edge => edge.source === currentId)
for (const edge of outgoingEdges)
if (dfs(edge.target))
return true
return false
}
return dfs(targetId)
}
if (checkIfCycleExists(connection.source, connection.target))
return false
const sourceNode = nodes.find(node => node.id === connection.source)
const targetNode = nodes.find(node => node.id === connection.target)
if (!sourceNode || !targetNode)
return false
const sourceIndex = parseInt(connection.sourceHandle.split('-')[1], 10)
const targetIndex = parseInt(connection.targetHandle.split('-')[1], 10)
const sourceType = sourceNode.data.outputs[sourceIndex].replace('$/', '')
const targetType = targetNode.data.inputs[targetIndex].replace('$/', '')
for (let t of sourceType.split('/'))
if (targetType.includes(t))
return true
return false
}, [nodes, edges])
function handleClear() {
if ((nodes.length > 0 || edges.length > 0) && window.confirm("Очистить холст?")) {
setNodes([])
@ -129,7 +359,6 @@ function App() {
})
}, [nodes])
return(
<div className="App">
<ControlPanel
@ -137,10 +366,7 @@ function App() {
onExportSvg = {handleExportSvg}
/>
<div className = "main-container">
<Toolbar
activeEdgeType = {edgeType}
onChangeEdgeType = {setEdgeType}
/>
<Toolbar/>
<div className = "main-canvas-container">
<div className="canvas-container">
<ReactFlow
@ -153,6 +379,7 @@ function App() {
onInit = {setFlowInstance}
onDrop = {onDrop}
onDragOver = {onDragOver}
isValidConnection = {isValidConnection}
//fitView
>
<Controls/>
@ -161,6 +388,11 @@ function App() {
</div>
</div>
</div>
<ModalData
modalData = {modalData}
onClose = {() => setModalData(null)}
onSave = {handleSaveModalData}
/>
</div>
)
}

View File

@ -2,12 +2,19 @@ import React, { memo } from 'react';
import { Handle, Position, useEdges } from '@xyflow/react';
function CustomBlock({ id: blockId, data }) {
const {type, label, inputCount = 2, outputCount = 1 } = data
const {type, label, inputs = [], outputs = [], metaFromType, metaToType } = data
const edges = useEdges()
const countForHeight = Math.max(inputCount, outputCount, 2)
const blockHeight = 50 + (countForHeight - 1) * 34
const blockWidth = blockHeight / 1.4
const inputCount = inputs.length
const outputCount = outputs.length
let countForHeight = Math.max(inputCount, outputCount, 2)
let blockHeight = 50 + (countForHeight - 1) * 34
let blockWidth = 60
if (['input', 'output', 'const', 'switch_type'].includes(type)) {
countForHeight = Math.max(inputCount, outputCount)
blockHeight = 40 + (countForHeight - 1) * 34
blockWidth = blockHeight * 2
}
function getPortStyle(portType, portIndex) {
const used = edges.some(edge => {
@ -36,6 +43,13 @@ function CustomBlock({ id: blockId, data }) {
const coords = []
if (count === 1)
coords.push(50)
else if (count < countForHeight) {
const step = blockHeight / (count + 1)
for (let i = 0; i < count; i++) {
const y = (i + 1) * step
coords.push((y / blockHeight) * 100)
}
}
else {
const step = (blockHeight - 50) / (count - 1)
for (let i = 0; i < count; i++) {
@ -49,6 +63,29 @@ function CustomBlock({ id: blockId, data }) {
const inputsCoords = getCoords(inputCount)
const outputsCoords = getCoords(outputCount)
function getAddElements() {
if (['ton', 'tof', 'tp'].includes(type))
return(
<>
<div className = "block-inputs-title" style = {{transform: `translate(0px, ${inputsCoords[0]}px)`}}>in</div>
<div className = "block-inputs-title" style = {{transform: `translate(0px, ${inputsCoords[1]}px)`}}>pt</div>
<div className = "block-outputs-title" style = {{transform: `translate(0px, ${outputsCoords[0]}px)`}}>q</div>
<div className = "block-outputs-title" style = {{transform: `translate(0px, ${outputsCoords[1]}px)`}}>et</div>
</>
)
if (type === 'ctu')
return {/*cu r pv, q cv*/}
if (type === 'ctd')
return {/*cd ld pv, q cv*/}
if (type === 'ctud')
return {/*cu cd r ld pv, qu qd cv*/}
if (type === 'rs')
return {/*s1 r, q1*/}
if (type === 'sr')
return {/*s r1, q*/}
return
}
return(
<div
className = "custom-fbd-block"
@ -57,7 +94,8 @@ function CustomBlock({ id: blockId, data }) {
height: `${blockHeight}px`
}}
>
<div className = "block-title" style = {{ paddingLeft: type === 'or' ? '32px' : '24px' }}>{label}</div>
<div className = "block-title">{label}</div>
{getAddElements()}
{inputsCoords.map((coord, ind) => (
<Handle

View File

@ -0,0 +1,171 @@
import React, { useState, useEffect } from 'react'
function ModalData({ modalData, onClose, onSave }) {
const [inputValue, setInputValue] = useState('')
const [fromType, setFromType] = useState('INT')
const [toType, setToType] = useState('REAL')
useEffect(() => {
setInputValue('')
setFromType('INT')
setToType('REAL')
}, [modalData])
if (!modalData)
return null
function getTypeValue() {
const value = inputValue.toLowerCase().trim()
const timeRegex = /^(t|time)#(-)?\d+(d|h|m|s|ms)(\d+(d|h|m|s|ms))*/
const realRegex = /^-?\d+\.\d+$/
const intRegex = /^-?\d+$/
if (value === 'true' || value === 'false')
return 'BOOL'
if (timeRegex.test(value))
return 'TIME'
if (value.startsWith("'") && value.endsWith("'"))
return 'STRING'
if (realRegex.test(value))
return 'REAL'
if (intRegex.test(value)) {
const num = Number(value)
if (num >= -32768 && num <= 32767)
return 'INT'
if (num >= -2147483648 && num <= 2147483647)
return 'DINT'
}
return null
}
function handleConfirm() {
const trimmed = inputValue.trim()
if (modalData.type === 'const') {
if (trimmed === '') {
alert('Переменная пуста')
return
}
}
else if (trimmed === '' && (modalData.type === 'input' || modalData.type === 'output')) {
alert('Не заданно имя переменной')
return
}
if (modalData.type === 'const') {
const getedType = getTypeValue()
if (!getedType) {
alert('Не удалось получить тип переменной (не корректный ввод)')
return
}
setToType(getedType)
}
onSave({
value: trimmed,
fromType,
toType
})
}
function getWindowWithType() {
const types = ["BOOL", "INT", "DINT", "REAL", "STRING", "TIME"]
switch(modalData.type) {
case 'switch_type':
return(
<div style = {{ marginBottom: '15px' }}>
<label style = {{ fontSize: '12px', display: 'block', marginBottom: '5px' }}>Входной тип данных:</label>
<select
value = {fromType}
onChange={(e) => setFromType(e.target.value)}
style={{ width: '100%', padding: '6px', marginBottom: '10px' }}
>
{types.map((type) => (
<option key = {`from-${type}`} value = {type}>{type}</option>
))}
</select>
<label style = {{ fontSize: '12px', display: 'block', marginBottom: '5px' }}>Выходной тип данных:</label>
<select
value = {toType}
onChange={(e) => setToType(e.target.value)}
style={{ width: '100%', padding: '6px' }}
>
{types.map((type) => (
<option key = {`from-${type}`} value = {type}>{type}</option>
))}
</select>
</div>
)
case 'const':
return(
<div style = {{ marginBottom: '15px' }}>
<label style = {{ fontSize: '12px', display: 'block', marginBottom: '5px' }}>Значение константы:</label>
<input
type = "text"
value = {inputValue}
onChange = {(e) => setInputValue(e.target.value)}
style = {{ width: '100%', padding: '6px', boxSizing: 'border-box', marginBottom: '12px' }}
placeholder = {12.5}
autoFocus
/>
</div>
)
case 'input':
case 'output':
return(
<div style = {{ marginBottom: '15px' }}>
<label style = {{ fontSize: '12px', display: 'block', marginBottom: '5px' }}>Имя переменной/тега:</label>
<input
type = "text"
value = {inputValue}
onChange = {(e) => setInputValue(e.target.value)}
style = {{ width: '100%', padding: '6px', boxSizing: 'border-box', marginBottom: '12px' }}
placeholder = {'Name'}
autoFocus
/>
<label style = {{ fontSize: '12px', display: 'block', marginBottom: '5px' }}>Тип данных:</label>
<select
value = {fromType}
onChange = {(e) => setFromType(e.target.value)}
style = {{ width: '100%', padding: '6px' }}
>
{types.map((type) => (
<option key={`from-${type}`} value = {type}>{type}</option>
))}
</select>
</div>
)
default:
return;
}
}
return(
<div className = "modalDataBack">
<div className = "modalDataMain">
<h4 style = {{ marginTop: 0, marginBottom: '15px'}}>Настройка: {modalData.label}</h4>
{getWindowWithType()}
<div style = {{ display: 'flex', justifyContent: 'flex-end', gap: '10px', marginTop: '20px' }}>
<button
onClick = {onClose}
style = {{ padding: '6px 12px', cursor: 'pointer'}}
>Отмена</button>
<button
onClick = {handleConfirm}
style = {{
padding: '6px 12px',
background: '#1717D8',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>ОК</button>
</div>
</div>
</div>
)
}
export default ModalData

View File

@ -1,5 +1,5 @@
import React, { useState, useMemo, useLayoutEffect, useRef } from 'react'
import { ToolbarBlock, ToolbarLine } from './ToolbarElements'
import {ToolbarBlock} from './ToolbarElements'
const NAME_CATEGORIES = {
logical: 'Логические',
@ -8,38 +8,45 @@ const NAME_CATEGORIES = {
counters: 'Счётчики',
comparisons: 'Сравнения',
other: 'Другие',
data: 'Данные',
customized: 'Пользовательские'
};
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 }],
{ type: 'and', label: 'AND', inputs: ['BOOL', 'BOOL'], outputs: ['BOOL'] },
{ type: 'or', label: 'OR', inputs: ['BOOL', 'BOOL'], outputs: ['BOOL'] },
{ type: 'not', label: 'NOT', inputs: ['BOOL'], outputs: ['BOOL'] },
{ type: 'xor', label: 'XOR', inputs: ['BOOL', 'BOOL'], outputs: ['BOOL'] }],
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 }],
{ type: 'add', label: 'ADD', inputs: ['$/INT/DINT/REAL', '$/INT/DINT/REAL'], outputs: ['$/INT/DINT/REAL'] },
{ type: 'sub', label: 'SUB', inputs: ['$/INT/DINT/REAL', '$/INT/DINT/REAL'], outputs: ['$/INT/DINT/REAL'] },
{ type: 'mul', label: 'MUL', inputs: ['$/INT/DINT/REAL', '$/INT/DINT/REAL'], outputs: ['$/INT/DINT/REAL'] },
{ type: 'div', label: 'DIV', inputs: ['$/INT/DINT/REAL', '$/INT/DINT/REAL'], outputs: ['$/INT/DINT/REAL'] }],
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 }],
{ type: 'ton', label: 'TON', inputs: ['BOOL', 'TIME'], outputs: ['BOOL', 'TIME'] },
{ type: 'tof', label: 'TOF', inputs: ['BOOL', 'TIME'], outputs: ['BOOL', 'TIME'] },
{ type: 'tp', label: 'TP', inputs: ['BOOL', 'TIME'], outputs: ['BOOL', 'TIME'] }],
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 }],
{ type: 'ctu', label: 'CTU', inputs: ['BOOL', 'BOOL', '$/INT/DINT'], outputs: ['BOOL', '$/INT/DINT'] },
{ type: 'ctd', label: 'CTD', inputs: ['BOOL', 'BOOL', '$/INT/DINT'], outputs: ['BOOL', '$/INT/DINT'] },
{ type: 'ctud', label: 'CTUD', inputs: ['BOOL', 'BOOL', 'BOOL', 'BOOL', '$/INT/DINT'], outputs: ['BOOL', 'BOOL', '$/INT/DINT'] }],
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 }],
{ type: 'gt', label: '>', inputs: ['$/INT/DINT/REAL/TIME', '$/INT/DINT/REAL/TIME'], outputs: ['BOOL'] },
{ type: 'lt', label: '<', inputs: ['$/INT/DINT/REAL/TIME', '$/INT/DINT/REAL/TIME'], outputs: ['BOOL'] },
{ type: 'eq', label: '=', inputs: ['$/BOOl/INT/DINT/REAL/TIME/STRING', '$/BOOl/INT/DINT/REAL/TIME/STRING'], outputs: ['BOOL'] }],
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: []
};
{ type: 'sr', label: 'SR', inputs: ['BOOL', 'BOOL'], outputs: ['BOOL'] },
{ type: 'rs', label: 'RS', inputs: ['BOOL', 'BOOL'], outputs: ['BOOL'] },
{ type: 'move', label: 'MOVE', inputs: ['$/BOOl/INT/DINT/REAL/TIME/STRING'], outputs: ['$/BOOl/INT/DINT/REAL/TIME/STRING'] }],
data: [
{ type: 'input', label: 'input', inputs: [], outputs: ['BOOl/INT/DINT/REAL/TIME/STRING'] },
{ type: 'output', label: 'output', inputs: ['BOOl/INT/DINT/REAL/TIME/STRING'], outputs: [] },
{ type: 'const', label: 'const', inputs: [], outputs: ['BOOl/INT/DINT/REAL/TIME/STRING'] },
{ type: 'switch_type', label: '*_TO_**', inputs: ['BOOl/INT/DINT/REAL/TIME/STRING'], outputs: ['BOOl/INT/DINT/REAL/TIME/STRING'] }
]
// customized: []
}
const LINES_TYPES = [
{ type: 'bool', stroke: '#000', dasharray: '0' },
@ -50,7 +57,9 @@ const LINES_TYPES = [
{ type: 'e', stroke: '#1717D8', dasharray: '0' } // Синяя
];
function Toolbar({ activeEdgeType, onChangeEdgeType }) {
const BLOCK_SCALE = 1
function Toolbar() {
const [activeTab, setActiveTab] = useState('logical')
const containerRef = useRef(null)
@ -75,11 +84,16 @@ function Toolbar({ activeEdgeType, onChangeEdgeType }) {
}, [])
function onDragStart(event, block) {
const offsetX = event.nativeEvent.offsetX * BLOCK_SCALE
const offsetY = event.nativeEvent.offsetY * BLOCK_SCALE
event.dataTransfer.setData('application/reactflow', JSON.stringify({
type: block.type,
label: block.label,
inputCount: block.inputCount,
outputCount: block.outputCount
inputs: block.inputs,
outputs: block.outputs,
offsetX,
offsetY
}))
event.dataTransfer.effectAllowed = 'move'
}
@ -94,7 +108,11 @@ function Toolbar({ activeEdgeType, onChangeEdgeType }) {
<button
key = {tab}
className = { activeTab === tab ? 'activeTab' : 'notActiveTab' }
data-row = { ['customized', 'other', 'comparisons'].includes(tab) ? '2' : '1' }
data-row = {
['logical', 'arithmetic', 'timers', 'counters'].includes(tab) ? '1' :
['comparisons', 'other', 'data'].includes(tab) ? '2' :
'3'
}
onClick = {() => setActiveTab(tab)}
title = {NAME_CATEGORIES[tab]}
> { NAME_CATEGORIES[tab] } </button>
@ -108,19 +126,7 @@ function Toolbar({ activeEdgeType, onChangeEdgeType }) {
key = {block.type}
block = {block}
onDragStart = {onDragStart}
/>
))}
</div>
{/*Соединения*/}
<div className = "toolbarLines" ref = {containerRef}>
{LINES_TYPES.map((line) => (
<ToolbarLine
key = {line.type}
line = {line}
caseWidth = {caseWidth}
isActive = {activeEdgeType === line.type}
onClick = {onChangeEdgeType}
scale = {BLOCK_SCALE}
/>
))}
</div>

View File

@ -1,15 +1,28 @@
import React, {memo} from 'react';
export const ToolbarBlock = memo(({ block, onDragStart }) => {
const scale = 1
const countForHeight = Math.max(block.inputCount, block.outputCount, 2)
const blockHeight = (50 + (countForHeight - 1) * 34) / scale
const blockWidth = blockHeight / 1.4
export const ToolbarBlock = memo(({ block, onDragStart, scale = 1 }) => {
const inputCount = block.inputs.length
const outputCount = block.outputs.length
let countForHeight = Math.max(inputCount, outputCount, 2)
let blockHeight = (50 + (countForHeight - 1) * 34) / scale
let blockWidth = 60 / scale
if (['input', 'output', 'const', 'switch_type'].includes(block.type)) {
countForHeight = Math.max(inputCount, outputCount)
blockHeight = (40 + (countForHeight - 1) * 34) / scale
blockWidth = blockHeight * 2
}
function getCoords(count) {
const coords = []
if (count === 1)
coords.push(50)
else if (count < countForHeight) {
const step = blockHeight / (count + 1)
for (let i = 0; i < count; i++) {
const y = (i + 1) * step
coords.push((y / blockHeight) * 100)
}
}
else {
const step = (blockHeight - (50 / scale)) / (count - 1)
for (let i = 0; i < count; i++) {
@ -20,8 +33,8 @@ export const ToolbarBlock = memo(({ block, onDragStart }) => {
return coords
}
const inputsCoords = getCoords(block.inputCount)
const outputsCoords = getCoords(block.outputCount)
const inputsCoords = getCoords(inputCount)
const outputsCoords = getCoords(outputCount)
return (
<div
@ -33,7 +46,7 @@ export const ToolbarBlock = memo(({ block, onDragStart }) => {
height: `${blockHeight}px`
}}
>
<div className = "preview-title" style = {{ paddingLeft: block.type === 'or' ? '32px' : '24px' }}>{block.label}</div>
<div className = "preview-title" style = {{ fontSize: `${13 / scale}px` }}>{block.label}</div>
{inputsCoords.map((coord, ind) => (
<div key = {`t-in-${ind}`}
className = "preview-port"
@ -59,27 +72,4 @@ export const ToolbarBlock = memo(({ block, onDragStart }) => {
))}
</div>
);
})
export function ToolbarLine({ line, caseWidth, isActive, onClick }) {
return (
<button
className={isActive ? 'activeEdgeTab' : 'notActiveEdgeTab'}
onClick={() => onClick && onClick(line.type)}
style={{
width: `${caseWidth}px`,
height: `${caseWidth / 2.5}px`
}}
>
<svg>
<line
x1 = "13%" y1 = "50%"
x2 = "87%" y2 = "50%"
stroke = {line.stroke}
strokeWidth = "2"
strokeDasharray = {line.dasharray}
/>
</svg>
</button>
)
}
})