Добавлена валидация (циклы)
This commit is contained in:
parent
99f520a672
commit
fffca3af9a
63
src/App.css
63
src/App.css
@ -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;
|
||||
}
|
||||
|
||||
@ -235,3 +267,26 @@ 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);
|
||||
}
|
||||
276
src/App.jsx
276
src/App.jsx
@ -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) => {
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
171
src/components/ModalData.jsx
Normal file
171
src/components/ModalData.jsx
Normal 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
|
||||
@ -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>
|
||||
|
||||
@ -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"
|
||||
@ -60,26 +73,3 @@ 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>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user