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 (
+
+
+