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
+