import React, { useState, useCallback } from 'react'
import { ReactFlow, Controls, Background, useNodesState, useEdgesState, addEdge, BackgroundVariant, getNodesBounds } from '@xyflow/react'
import { toSvg } from 'html-to-image'
import '@xyflow/react/dist/style.css'
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
}
const EDGE_STYLES = {
bool: { stroke: '#000', 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)
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 (edge.source == 1000000006)
if (sourceType === targetType && edge.data.type === sourceType.toLowerCase())
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,
data: {type: newType.toLowerCase().split('/')[0]}
}
}
return ed
})
return {nextNodes, nextEdges, change}
}
function updateGraphTypes(currentEdges, currentNodes) {
let globalChange = true
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}
}
const onConnect = useCallback(
(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: EDGE_STYLES[sourceType.toLowerCase().split('/')[0]] || EDGE_STYLES['bool'],
data: { type: sourceType.toLowerCase().split('/')[0] }
}
const allEdges = [...edges, newEdge]
const { currentNodes, currentEdges } = updateGraphTypes(allEdges, [...nodes])
setNodes(currentNodes)
setEdges(currentEdges)
}, [nodes, setNodes, setEdges, edgeType]
)
const onDragOver = useCallback((event) => {
event.preventDefault()
event.dataTransfer.dropEffect = 'move'
}, [])
const onDrop = useCallback(
(event) => {
event.preventDefault()
if (!flowInstance)
return
const dataStr = event.dataTransfer.getData('application/reactflow')
if (!dataStr)
return
const {type, label, width, height, inputs = [], outputs = [],
inputsLabels = [], outputsLabels = [], offsetX = 0, offsetY = 0} = JSON.parse(dataStr)
const position = flowInstance.screenToFlowPosition({
x: event.clientX - offsetX,
y: event.clientY - offsetY
})
if (['const', 'switch_type', 'input', 'output'].includes(type)) {
setModalData({ type, label, width, height, position, inputs, outputs, inputsLabels, outputsLabels })
return
}
const newNode = {
id: `${Date.now()}`,
type: 'fbdBlock',
position,
data: { type, label, width, height, inputs, outputs, inputsLabels, outputsLabels }
}
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.toType})`
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: `${Date.now()}`,
type: 'fbdBlock',
position: modalData.position,
data: {
type: modalData.type,
label: finalLabel,
width: modalData.width,
height: modalData.height,
value: formData.value,
inputs,
outputs,
inputsLabels: modalData.inputsLabels,
outputsLabels: modalData.outputsLabels,
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([])
setEdges([])
}
}
const handleExportSvg = useCallback(() => {
const reactFlowViewport = document.querySelector('.react-flow__viewport')
if (!reactFlowViewport || !nodes.length > 0) {
alert('Не удалось найти холст для экспорта')
return
}
const bounds = getNodesBounds(nodes)
const padding = 30
const widthExportWindow = bounds.width + padding * 2
const heightExportWindow = bounds.height + padding * 2
toSvg(reactFlowViewport, {
backgroundColor: 'white',
width: widthExportWindow,
height: heightExportWindow,
style: {
width: `${widthExportWindow}px`,
height: `${heightExportWindow}px`,
transform: `translate(${-bounds.x + padding}px, ${-bounds.y + padding}px) scale(1)`,
transformOrigin: 'top left'
}
}).then(async (dataUrl) => {
if ('showSaveFilePicker' in window) {
try {
const handle = await window.showSaveFilePicker({
suggestedName: 'fbd-scheme.svg',
types: [{
description: 'Векторная диаграмма SVG',
accept: {'image/svg+xml': ['.svg']}
}]
})
const writable = await handle.createWritable()
const scheme = await fetch(dataUrl)
const file = await scheme.blob()
await writable.write(file)
await writable.close()
}
catch (err) { console.log('Пользователь отменил сохранение или произошла ошибка: ', err) }
}
else {
const link = document.createElement('a')
link.download = 'fbd-scheme.svg'
link.href = dataUrl
link.click()
}
}).catch((error) => {
console.error('Ошибка генерации SVG-файла: ', error)
alert('Не удалось сгененировать SVG-файл')
})
}, [nodes])
const handleExportXml = useCallback(async () => {
if (nodes.length === 0) {
alert('На холсте нет блоков для экспорта')
return
}
let xml = `\n`
xml += `\n`
xml += ` \n`
xml += ` \n`
xml += ` \n`
xml += ` \n`
xml += ` \n`
xml += ` \n`
xml += ` \n`
xml += ` \n`
xml += ` \n`
xml += ` \n`
xml += ` \n`
xml += ` \n`
function getPortType(type) {
const newType = type.toUpperCase().replace('$/', '')
if (!newType.includes('/'))
return newType
const types = newType.split('/')
const hasAll = (...neededTypes) => neededTypes.every(t => types.includes(t))
if (hasAll('BOOL', 'INT', 'DINT', 'REAL', 'TIME', 'STRING'))
return 'ANY'
if (hasAll('INT', 'DINT', 'REAL', 'TIME'))
return 'ANY_MAGNITUDE'
if (hasAll('INT', 'DINT', 'REAL'))
return 'ANY_NUM'
if (hasAll('INT', 'DINT'))
return 'ANY_INT'
return 'ANY'
}
let dopInd = 1
const nodesIndexes = new Map()
nodes.forEach(node => {
let counts = 0
if ((['input', 'const'].includes(node.data.type)) && node.data.value) {
edges.forEach(edge => {
if (edge.source === node.id)
counts += 1
})
}
else if (node.data.type === 'output' && node.data.value) {
edges.forEach(edge => {
if (edge.target === node.id)
counts += 1
})
}
else
counts += 1
if (counts !== 0) {
nodesIndexes.set(node.id, [1000000000 + dopInd])
dopInd += 1
}
for (let ind = 1; ind < counts; ind++, dopInd++)
nodesIndexes.get(node.id).push(1000000000 + dopInd)
})
function getNewBlockId(id) {
const newInd = nodesIndexes.get(id)
if (newInd.length > 1) {
const t = nodesIndexes.get(id).splice(0, 1)[0]
return t
}
else if (newInd)
return newInd[0]
dopInd += 1
return 1000000000 + dopInd
}
xml += ` \n`
const valuesIn = new Map()
const valuesOut = new Map()
nodes.forEach(node => {
if (node.data.type === 'input' && node.data.value)
valuesIn.set(node.data.value, node.data.metaFromType || 'ANY')
else if (node.data.type === 'output' && node.data.value)
valuesOut.set(node.data.value, node.data.metaFromType || 'ANY')
})
if (valuesIn) {
xml += ` \n`
valuesIn.forEach((type, value) => {
xml += ` \n`
xml += ` <${getPortType(type)}/>\n`
xml += ` \n`
})
xml += ` \n`
}
if (valuesOut) {
xml += ` \n`
valuesOut.forEach((type, value) => {
xml += ` \n`
xml += ` <${getPortType(type)}/>\n`
xml += ` \n`
})
xml += ` \n`
}
xml += ` \n`
xml += ` \n`
xml += ` \n\n`
nodes.forEach(node => {
const newInd = nodesIndexes.get(node.id)
if (node && node.data.type === 'input' || node.data.type === 'const') {
for (let i = 0; i < newInd.length; i++) {
xml += ` \n`
xml += ` \n`
xml += ` \n`
xml += ` ${node.data.value}\n`
xml += ` \n\n`
}
}
})
nodes.forEach(node => {
const newInd = nodesIndexes.get(node.id)
if (node.data.type === 'output') {
for (let i = 0; i < newInd.length; i++) {
xml += ` \n`
xml += ` \n`
xml += ` \n`
edges.forEach(edge => {
if (edge.target === node.id) {
const targetIndex = parseInt(edge.targetHandle.split('-')[1], 10)
const targetLabel = node.data.inputsLabels[targetIndex]
if (targetLabel === 'IN') {
const sourceNode = nodes.find(node => node.id === edge.source)
if (['const', 'input'].includes(sourceNode.data.type))
xml += ` \n`
else {
const sourceIndex = parseInt(edge.sourceHandle.split('-')[1], 10)
const sourceLabel = sourceNode.data.outputsLabels[sourceIndex]
xml += ` \n`
}
}
}
})
xml += ` \n`
xml += ` ${node.data.value}\n`
xml += ` \n\n`
}
}
})
nodes.forEach(node => {
if (!['output', 'input', 'const'].includes(node.data.type)) {
xml += ` \n`
xml += ` \n`
xml += ` \n`
for (const portType of node.data.inputsLabels) {
xml += ` \n`
xml += ` \n`
edges.forEach(edge => {
if (edge.target === node.id) {
const targetIndex = parseInt(edge.targetHandle.split('-')[1], 10)
const targetLabel = node.data.inputsLabels[targetIndex]
if (targetLabel === portType) {
const sourceNode = nodes.find(node => node.id === edge.source)
if (['const', 'input'].includes(sourceNode.data.type))
xml += ` \n`
else {
const sourceIndex = parseInt(edge.sourceHandle.split('-')[1], 10)
const sourceLabel = sourceNode.data.outputsLabels[sourceIndex]
xml += ` \n`
}
}
}
})
xml += ` \n`
xml += ` \n`
}
xml += ` \n`
xml += ` \n`
xml += ` \n`
for (const portType of node.data.outputsLabels) {
xml += ` \n`
xml += ` \n`
xml += ` \n`
}
xml += ` \n`
xml += ` \n\n`
}
})
xml += ` \n`
xml += ` \n`
xml += ` \n`
xml += ` \n`
xml += ` \n`
xml += ` \n`
xml += ` \n`
xml += ` \n`
xml += ``
const file = new Blob([xml], {type: 'application/xml'})
if ('showSaveFilePicker' in window) {
try {
const handle = await window.showSaveFilePicker({
suggestedName: 'fbd-scheme.xml',
types: [{
description: 'FBD диаграмма в XML',
accept: {'application/xml': ['.xml']}
}]
})
const writable = await handle.createWritable()
await writable.write(file)
await writable.close()
}
catch (err) {
console.log('Сохранение XML отменено или произошла ошибка:', err)
}
}
else {
const link = document.createElement('a')
link.download = 'fbd-program.xml'
link.href = URL.createObjectURL(file)
link.click()
URL.revokeObjectURL(link.href)
}
}, [nodes, edges])
const handleImportXml = useCallback((event) => {
const file = event.target.files[0]
if (!file)
return
const reader = new FileReader()
reader.onload = (e) => {
const xmlText = e.target.result
const parser = new DOMParser()
const xml = parser.parseFromString(xmlText, 'application/xml')
if (xml.querySelector('parsererror')) {
alert('Ошибка импорта (невалидный XML-код)')
return
}
const importedNodes = []
const importedEdges = []
const xmlIdToReactId = new Map()
const interfaceVars = new Map() // name: { dataType, initialValue, varGroup }
const xmlVariables = xml.querySelectorAll('interface * > variable')
xmlVariables.forEach(v => {
const name = v.getAttribute('name')
const varType = v.parentElement.tagName.toLowerCase()
const type = v.querySelector('type *')
const value = v.querySelector('initialValue simpleValue')?.getAttribute('value') || ''
if (name && type)
interfaceVars.set(name, {
dataType: type.tagName.toUpperCase(),
value,
varType
})
})
function restoreType(recivedType) {
const type = recivedType.toUpperCase()
if (type === 'ANY') return 'BOOl/INT/DINT/REAL/TIME/STRING'
if (type === 'ANY_MAGNITUDE') return 'INT/DINT/REAL/TIME'
if (type === 'ANY_NUM') return 'INT/DINT/REAL'
if (type === 'ANY_INT') return 'INT/DINT'
return type
}
const xmlInVariables = xml.querySelectorAll('inVariable')
xmlInVariables.forEach(inVar => {
const xmlId = inVar.getAttribute('localId')
const width = parseFloat(inVar.getAttribute('width') || '80')
const height = parseFloat(inVar.getAttribute('height') || '40')
const pos = inVar.querySelector('position')
const x = parseFloat(pos?.getAttribute('x') || '0')
const y = parseFloat(pos?.getAttribute('y') || '0')
const value = inVar.querySelector('expression')?.textContent || ''
function isItConst(recivedValue) {
const value = recivedValue.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
}
let isConst = isItConst(value) ? true : false
let varInfo = interfaceVars.get(value)
const outputPortType = varInfo?.dataType || isItConst(value) || 'ANY'
if (varInfo?.varType === 'tempvars')
isConst = true
const finalType = isConst ? 'const' : 'input'
let finalValue = value
if (varInfo?.varType === 'tempvars' && varInfo.value)
finalValue = varInfo.value
let existingNode = importedNodes.find(node => node.position.x === x && node.position.y === y && node.data.value === finalValue)
if (!existingNode) {
const newId = `${xmlId}`
const label = finalType === 'const' ? `${finalValue} (${outputPortType})` : `${finalValue} (${outputPortType})`
existingNode = {
id: newId,
type: 'fbdBlock',
position: {x, y},
data: {
type: finalType, label,
value: finalValue,
metaFromType: outputPortType,
width, height,
inputs: [],
outputs: [restoreType(outputPortType)],
inputsLabels: [],
outputsLabels: ['OUT']
}
}
importedNodes.push(existingNode)
}
xmlIdToReactId.set(xmlId, {reactId: existingNode.id, portIndex: 0, handleType: 'out'})
})
const xmlOutVariables = xml.querySelectorAll('outVariable')
xmlOutVariables.forEach(outVar => {
const xmlId = outVar.getAttribute('localId')
const width = parseFloat(outVar.getAttribute('width') || '80')
const height = parseFloat(outVar.getAttribute('height') || '40')
const pos = outVar.querySelector('position')
const x = parseFloat(pos?.getAttribute('x') || '0')
const y = parseFloat(pos?.getAttribute('y') || '0')
const value = outVar.querySelector('expression')?.textContent || ''
let varInfo = interfaceVars.get(value)
const dataType = varInfo?.dataType || 'ANY'
let existingNode = importedNodes.find(node => node.position.x === x && node.position.y === y && node.data.value === value)
if (!existingNode) {
existingNode = {
id: xmlId,
type: 'fbdBlock',
position: {x, y},
data: {
type: 'output',
label: `${value} (${dataType})`,
value,
metaFromType: dataType,
width, height,
inputs: [restoreType(dataType)],
outputs: [],
inputsLabels: ['IN'],
outputsLabels: []
}
}
importedNodes.push(existingNode)
}
xmlIdToReactId.set(xmlId, {reactId: existingNode.id, portIndex: 0, handleType: 'in'})
const connection = outVar.querySelector('connectionPointIn connection')
if (connection) {
const sourceNode = connection.getAttribute('refLocalId')
const sourcePortLabel = connection.getAttribute('formalParameter')
importedEdges.push({
xmlSourceNode: sourceNode,
xmlSourcePortLabel: sourcePortLabel,
reactTargetId: existingNode.id,
reactTargetPort: 'in-0'
})
}
})
const libraryBlocks = {
and: { label: 'AND', inputs: ['BOOL', 'BOOL'], outputs: ['BOOL'], inputsLabels: ['IN1', 'IN2'], outputsLabels: ['OUT'] },
or: { label: 'OR', inputs: ['BOOL', 'BOOL'], outputs: ['BOOL'], inputsLabels: ['IN1', 'IN2'], outputsLabels: ['OUT'] },
not: { label: 'NOT', inputs: ['BOOL'], outputs: ['BOOL'], inputsLabels: ['IN'], outputsLabels: ['OUT'] },
xor: { label: 'XOR', inputs: ['BOOL', 'BOOL'], outputs: ['BOOL'], inputsLabels: ['IN1', 'IN2'], outputsLabels: ['OUT'] },
add: { label: 'ADD', inputs: ['$/INT/DINT/REAL', '$/INT/DINT/REAL'], outputs: ['$/INT/DINT/REAL'], inputsLabels: ['IN1', 'IN2'], outputsLabels: ['OUT'] },
sub: { label: 'SUB', inputs: ['$/INT/DINT/REAL', '$/INT/DINT/REAL'], outputs: ['$/INT/DINT/REAL'], inputsLabels: ['IN1', 'IN2'], outputsLabels: ['OUT'] },
mul: { label: 'MUL', inputs: ['$/INT/DINT/REAL', '$/INT/DINT/REAL'], outputs: ['$/INT/DINT/REAL'], inputsLabels: ['IN1', 'IN2'], outputsLabels: ['OUT'] },
div: { label: 'DIV', inputs: ['$/INT/DINT/REAL', '$/INT/DINT/REAL'], outputs: ['$/INT/DINT/REAL'], inputsLabels: ['IN1', 'IN2'], outputsLabels: ['OUT'] },
ton: { label: 'TON', inputs: ['BOOL', 'TIME'], outputs: ['BOOL', 'TIME'], inputsLabels: ['IN', 'PT'], outputsLabels: ['Q', 'ET'] },
tof: { label: 'TOF', inputs: ['BOOL', 'TIME'], outputs: ['BOOL', 'TIME'], inputsLabels: ['IN', 'PT'], outputsLabels: ['Q', 'ET'] },
tp: { label: 'TP', inputs: ['BOOL', 'TIME'], outputs: ['BOOL', 'TIME'], inputsLabels: ['IN', 'PT'], outputsLabels: ['Q', 'ET'] },
ctu: { label: 'CTU', inputs: ['BOOL', 'BOOL', '$/INT/DINT'], outputs: ['BOOL', '$/INT/DINT'], inputsLabels: ['CU', 'R', 'PV'], outputsLabels: ['Q', 'CV'] },
ctd: { label: 'CTD', inputs: ['BOOL', 'BOOL', '$/INT/DINT'], outputs: ['BOOL', '$/INT/DINT'], inputsLabels: ['CD', 'LD', 'PV'], outputsLabels: ['Q', 'CV'] },
ctud: { label: 'CTUD', inputs: ['BOOL', 'BOOL', 'BOOL', 'BOOL', '$/INT/DINT'], outputs: ['BOOL', 'BOOL', '$/INT/DINT'], inputsLabels: ['CU', 'CD', 'R', 'LD', 'PV'], outputsLabels: ['QU', 'QD', 'CV'] },
gt: { label: '>', inputs: ['$/INT/DINT/REAL/TIME', '$/INT/DINT/REAL/TIME'], outputs: ['BOOL'], inputsLabels: ['IN1', 'IN2'], outputsLabels: ['OUT'] },
lt: { label: '<', inputs: ['$/INT/DINT/REAL/TIME', '$/INT/DINT/REAL/TIME'], outputs: ['BOOL'], inputsLabels: ['IN1', 'IN2'], outputsLabels: ['OUT'] },
eq: { label: '=', inputs: ['$/BOOl/INT/DINT/REAL/TIME/STRING', '$/BOOl/INT/DINT/REAL/TIME/STRING'], outputs: ['BOOL'], inputsLabels: ['IN1', 'IN2'], outputsLabels: ['OUT'] },
sr: { label: 'SR', inputs: ['BOOL', 'BOOL'], outputs: ['BOOL'], inputsLabels: ['S1', 'R'], outputsLabels: ['Q1'] },
rs: { label: 'RS', inputs: ['BOOL', 'BOOL'], outputs: ['BOOL'], inputsLabels: ['S', 'R1'], outputsLabels: ['Q'] },
move: { label: 'MOVE', inputs: ['$/BOOl/INT/DINT/REAL/TIME/STRING'], outputs: ['$/BOOl/INT/DINT/REAL/TIME/STRING'], inputsLabels: ['IN'], outputsLabels: ['OUT'] },
switch_type: { label: '*_TO_**', inputs: ['BOOl/INT/DINT/REAL/TIME/STRING'], outputs: ['BOOl/INT/DINT/REAL/TIME/STRING'], inputsLabels: ['IN'], outputsLabels: ['OUT'] }
}
const xmlBlocks = xml.querySelectorAll('FBD > block')
xmlBlocks.forEach(block => {
const xmlId = block.getAttribute('localId')
const xmlType = block.getAttribute('typeName')
const pos = block.querySelector('position')
const x = parseFloat(pos?.getAttribute('x') || '0')
const y = parseFloat(pos?.getAttribute('y') || '0')
const label = block.querySelector('content')?.textContent || libraryBlocks[xmlType.toLowerCase()]?.label || xmlType
const value = block.querySelector('additionalData')?.getAttribute('value') || ''
let inputsLabels = Array.from(block.querySelectorAll('inputVariables variable')).map(v => v.getAttribute('formalParameter'))
let inputs = Array.from(block.querySelectorAll('inputVariables variable')).map(v => restoreType(v.getAttribute('dataType') || 'ANY'))
let outputsLabels = Array.from(block.querySelectorAll('outputVariables variable')).map(v => v.getAttribute('formalParameter'))
let outputs = Array.from(block.querySelectorAll('outputVariables variable')).map(v => restoreType(v.getAttribute('dataType') || 'ANY'))
if (inputsLabels.length === libraryBlocks[xmlType.toLowerCase()]?.inputsLabels.length)
inputsLabels = libraryBlocks[xmlType.toLowerCase()].inputsLabels
if (outputsLabels.length === libraryBlocks[xmlType.toLowerCase()]?.outputsLabels.length)
outputsLabels = libraryBlocks[xmlType.toLowerCase()].outputsLabels
if (inputs.length === libraryBlocks[xmlType.toLowerCase()]?.inputs.length)
inputs = libraryBlocks[xmlType.toLowerCase()].inputs
if (outputs.length === libraryBlocks[xmlType.toLowerCase()]?.outputs.length)
outputs = libraryBlocks[xmlType.toLowerCase()].outputs
const isSwitchType = xmlType.includes('_TO_')
const finalType = isSwitchType ? 'switch_type' : xmlType.toLowerCase()
const width = parseFloat(block.getAttribute('width') || (isSwitchType ? '80' : 50 + (Math.max(inputs.length, outputs.length, 2) - 1) * 34))
const height = parseFloat(block.getAttribute('height') || (isSwitchType ? '40' : '60'))
const newNode = {
id: xmlId,
type: 'fbdBlock',
position: {x, y},
data: { type: finalType, label, value, width, height, inputs, outputs, inputsLabels, outputsLabels}
}
importedNodes.push(newNode)
xmlIdToReactId.set(xmlId, { reactId: xmlId, inputsLabels, outputsLabels })
const variable = block.querySelectorAll('inputVariables variable')
variable.forEach((v, inputIndex) => {
const connection = v.querySelector('connectionPointIn connection')
if (connection) {
const sourceNode = connection.getAttribute('refLocalId')
const sourcePortLabel = connection.getAttribute('formalParameter')
importedEdges.push({
xmlSourceNode: sourceNode,
xmlSourcePortLabel: sourcePortLabel,
reactTargetId: xmlId,
reactTargetPort: `in-${inputIndex}`
})
}
})
})
const finalEdges = importedEdges.map((edgeData, index) => {
const sourceMeta = xmlIdToReactId.get(edgeData.xmlSourceNode)
if (!sourceMeta)
return null
let sourceHandle = 'out-0'
if (sourceMeta.outputsLabels) {
const outIndex = sourceMeta.outputsLabels.indexOf(edgeData.xmlSourcePortLabel)
sourceHandle = `out-${outIndex >= 0 ? outIndex : 0}`
}
const sourceNode = importedNodes.find(node => node.id === sourceMeta.reactId)
const sourceIndex = parseInt(sourceHandle.split('-')[1], 10)
const sourceType = sourceNode?.data?.outputs[sourceIndex] || 'ANY'
return {
id: `e_${Date.now()}_${index}`,
source: sourceMeta.reactId,
target: edgeData.reactTargetId,
sourceHandle: sourceHandle,
targetHandle: edgeData.reactTargetPort,
type: 'smoothstep',
style: EDGE_STYLES[sourceType.toLowerCase().split('/')[0]] || EDGE_STYLES['bool'],
data: {type: sourceType.toLowerCase().split('/')[0]}
}
}).filter(Boolean)
const finalNodes = importedNodes.map(node => {
if (['add', 'sub', 'mul', 'div', 'move'].includes(node.data.type))
return {
...node,
data: {
...node.data,
inputs: node.data.inputs.map(i => '$/' + i),
outputs: node.data.outputs.map(i => '$/' + i)
}
}
else if (['ctu', 'ctd', 'ctud'].includes(node.data.type))
return {
...node,
data: {
...node.data,
inputs: node.data.inputs.map((i, ind) => {
if (node.data.inputsLabels.toLowerCase() === 'pv')
return '$/' + i
else return i
}),
outputs: node.data.outputs.map((i, ind) => {
if (node.data.inputsLabels.toLowerCase() === 'cv')
return '$/' + i
else return i
})
}
}
else if (['gt', 'lt', 'eq'].includes(node.data.type))
return {
...node,
data: {
...node.data,
inputs: node.data.inputs.map(i => '$/' + i)
}
}
else return node
})
try {
const { currentNodes, currentEdges } = updateGraphTypes(finalEdges, finalNodes)
setNodes(currentNodes)
setEdges(currentEdges)
}
catch {
alert('Произошла ошибка при обработке соединений')
setEdges([])
setNodes([finalNodes])
}
}
reader.readAsText(file)
event.target.value = ''
}, [nodes, edges])
return(
setModalData(null)}
onSave = {handleSaveModalData}
/>
)
}
export default App