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