diff --git a/src/App.css b/src/App.css index 6d77015..79a6a3f 100644 --- a/src/App.css +++ b/src/App.css @@ -115,10 +115,10 @@ html, body, #root { background-color: whitesmoke; border: 1.5px solid black; box-sizing: border-box; - position: relative; font-family: Arial, sans-serif; user-select: none; border-radius: 4px; + position: relative; } .custom-fbd-block .block-title { @@ -132,16 +132,20 @@ html, body, #root { .custom-fbd-block .block-inputs-title { box-sizing: border-box; + width: 20px; font-size: 10px; text-align: left; pointer-events: none; + position: absolute; } .custom-fbd-block .block-outputs-title { box-sizing: border-box; + width: 17px; font-size: 10px; text-align: right; pointer-events: none; + position: absolute; } diff --git a/src/App.jsx b/src/App.jsx index 334ad27..08011d9 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,12 +1,13 @@ 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 { toSvg, toPng } 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 Toolbar from './components/Toolbar' +import ControlPanel from './components/ControlPanel' +import CustomBlock from './components/CustomBlock' import ModalData from './components/ModalData' +import { convertGraphToXml, parseXmlToGraph } from './utils/xmlUtils' const nodeTypes = { fbdBlock: CustomBlock @@ -153,63 +154,59 @@ function App() { 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 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 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 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 onDrop = useCallback((event) => { + event.preventDefault() + if (!flowInstance) + return - const dataStr = event.dataTransfer.getData('application/reactflow') - if (!dataStr) - 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 - }) + 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 - } + if (['const', 'switch_type', 'input', 'output'].includes(type)) { + setModalData({ type, label, width: widthPrew, height: heightPrew, 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] - ) + const newNode = { + id: `${Date.now()}`, + type: 'fbdBlock', + position, + data: { type, label, width, height, inputs, outputs, inputsLabels, outputsLabels } + } + setNodes((currentNodes) => [...currentNodes, newNode]) + }, [flowInstance, setNodes]) const onNodesDelete = useCallback((deletedNodes) => { setNodes((nds) => nds.filter((node) => !deletedNodes.some((dn) => dn.id === node.id))) @@ -299,6 +296,9 @@ function App() { if (!sourceNode || !targetNode) return false + if (edges.find(edge => edge.target === targetNode)) + return false + const sourceIndex = parseInt(connection.sourceHandle.split('-')[1], 10) const targetIndex = parseInt(connection.targetHandle.split('-')[1], 10) @@ -319,9 +319,9 @@ function App() { } } - const handleExportSvg = useCallback(() => { + function handleExportSvg() { const reactFlowViewport = document.querySelector('.react-flow__viewport') - if (!reactFlowViewport || !nodes.length > 0) { + if (!reactFlowViewport || nodes.length === 0) { alert('Не удалось найти холст для экспорта') return } @@ -370,227 +370,78 @@ function App() { console.error('Ошибка генерации SVG-файла: ', error) alert('Не удалось сгененировать SVG-файл') }) - }, [nodes]) + } - const handleExportXml = useCallback(async () => { + function handleExportPng() { + const reactFlowViewport = document.querySelector('.react-flow__viewport') + + if (!reactFlowViewport || nodes.length === 0) { + alert('Не удалось найти холст или на нем нет элементов для экспорта') + return + } + + const bounds = getNodesBounds(nodes) + const padding = 7 + const widthExportWindow = bounds.width + padding * 2 + const heightExportWindow = bounds.height + padding * 2 + + toPng(reactFlowViewport, { + backgroundColor: null, + 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' + }, + filter: (node) => { + if (node?.classList?.contains('react-flow__background')) + return false + return true + } + }).then(async (dataUrl) => { + if ('showSaveFilePicker' in window) { + try { + const handle = await window.showSaveFilePicker({ + suggestedName: 'fbd-scheme.png', + types: [{ + description: 'Изображение PNG', + accept: {'image/png': ['.png']} + }] + }) + + 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.png' + link.href = dataUrl + link.click() + } + }).catch((error) => { + console.error('Ошибка генерации PNG-файла: ', error) + alert('Не удалось сгенерировать PNG-файл') + }) + } + + async function handleExportXml() { 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 xml = convertGraphToXml(nodes, edges) const file = new Blob([xml], {type: 'application/xml'}) + if ('showSaveFilePicker' in window) { try { const handle = await window.showSaveFilePicker({ @@ -616,9 +467,9 @@ function App() { link.click() URL.revokeObjectURL(link.href) } - }, [nodes, edges]) + } - const handleImportXml = useCallback((event) => { + function handleImportXml(event) { const file = event.target.files[0] if (!file) return @@ -633,289 +484,7 @@ function App() { 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 - }) + const { finalNodes, finalEdges } = parseXmlToGraph(xml) try { const { currentNodes, currentEdges } = updateGraphTypes(finalEdges, finalNodes) @@ -930,13 +499,14 @@ function App() { } reader.readAsText(file) event.target.value = '' - }, [nodes, edges]) + } return(
diff --git a/src/components/ControlPanel.jsx b/src/components/ControlPanel.jsx index 7202952..1a02c58 100644 --- a/src/components/ControlPanel.jsx +++ b/src/components/ControlPanel.jsx @@ -1,6 +1,6 @@ import React, {useRef} from 'react'; -function ControlPanel({ onClear, onExportSvg, onExportXml, onImportXml }) { +function ControlPanel({ onClear, onExportSvg, onExportPng, onExportXml, onImportXml }) { const fileInputRef = useRef(null) const handleImportButtonClick = () => { @@ -16,6 +16,12 @@ function ControlPanel({ onClear, onExportSvg, onExportXml, onImportXml }) { > Экспорт в SVG +