diff --git a/src/App.jsx b/src/App.jsx index 87239f4..840610d 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -29,6 +29,130 @@ function App() { 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) @@ -40,137 +164,13 @@ function App() { id: `e_${Date.now()}`, type: 'smoothstep', style: EDGE_STYLES[sourceType.toLowerCase().split('/')[0]] || EDGE_STYLES['bool'], - data: { type: edgeType } + data: { type: sourceType.toLowerCase().split('/')[0] } } const allEdges = [...edges, newEdge] - const { currentNodes, currentEdges } = updateGraphTypes(allEdges) + const { currentNodes, currentEdges } = updateGraphTypes(allEdges, [...nodes]) 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] ) @@ -189,22 +189,23 @@ function App() { if (!dataStr) return - const {type, label, inputs = [], outputs = [], offsetX = 0, offsetY = 0} = JSON.parse(dataStr) + 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, position, inputs, outputs }) + setModalData({ type, label, width, height, position, inputs, outputs, inputsLabels, outputsLabels }) return } const newNode = { - id: `block_${Date.now()}`, + id: `${Date.now()}`, type: 'fbdBlock', position, - data: { type, label, inputs, outputs } + data: { type, label, width, height, inputs, outputs, inputsLabels, outputsLabels } } setNodes((currentNodes) => [...currentNodes, newNode]) }, [flowInstance, setNodes] @@ -219,7 +220,7 @@ function App() { let outputs = [] if (modalData.type === 'const') { - finalLabel = `${formData.value} (${formData.fromType})` + finalLabel = `${formData.value} (${formData.toType})` outputs = [formData.fromType] } else if (modalData.type === 'switch_type') { @@ -237,15 +238,19 @@ function App() { } const newNode = { - id: `block_${Date.now()}`, + 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 } @@ -359,11 +364,540 @@ function App() { }) }, [nodes]) + const handleExportXml = useCallback(() => { + 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` + xml += ` \n` + + const values = new Map() + nodes.forEach(node => { + if ((node.data.type === 'input' || node.data.type === 'output') && node.data.value) + values.set(node.data.value, node.data.metaFromType || 'ANY') + }) + + values.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 += `` + + console.log(xml) + + const file = new Blob([xml], {type: 'application/xml'}) + 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, + 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, + 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(input.length, output.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) + console.log(currentNodes) + console.log(currentEdges) + } + catch { + alert('Произошла ошибка при обработке соединений') + setEdges([]) + setNodes([finalNodes]) + } + } + reader.readAsText(file) + event.target.value = '' + }, [nodes, edges]) + return(
diff --git a/src/components/ControlPanel.jsx b/src/components/ControlPanel.jsx index 9af52f1..7202952 100644 --- a/src/components/ControlPanel.jsx +++ b/src/components/ControlPanel.jsx @@ -1,6 +1,13 @@ -import React from 'react'; +import React, {useRef} from 'react'; + +function ControlPanel({ onClear, onExportSvg, onExportXml, onImportXml }) { + const fileInputRef = useRef(null) + + const handleImportButtonClick = () => { + if (fileInputRef.current) + fileInputRef.current.click(); + }; -function ControlPanel({ onClear, onExportSvg }) { return (
+ + +