отдельный компонент для xml
This commit is contained in:
parent
10e0bb373a
commit
fe53e63a18
@ -115,10 +115,10 @@ html, body, #root {
|
|||||||
background-color: whitesmoke;
|
background-color: whitesmoke;
|
||||||
border: 1.5px solid black;
|
border: 1.5px solid black;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
position: relative;
|
|
||||||
font-family: Arial, sans-serif;
|
font-family: Arial, sans-serif;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-fbd-block .block-title {
|
.custom-fbd-block .block-title {
|
||||||
@ -132,16 +132,20 @@ html, body, #root {
|
|||||||
|
|
||||||
.custom-fbd-block .block-inputs-title {
|
.custom-fbd-block .block-inputs-title {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
width: 20px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-fbd-block .block-outputs-title {
|
.custom-fbd-block .block-outputs-title {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
width: 17px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
672
src/App.jsx
672
src/App.jsx
@ -1,12 +1,13 @@
|
|||||||
import React, { useState, useCallback } from 'react'
|
import React, { useState, useCallback } from 'react'
|
||||||
import { ReactFlow, Controls, Background, useNodesState, useEdgesState, addEdge, BackgroundVariant, getNodesBounds } from '@xyflow/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 '@xyflow/react/dist/style.css'
|
||||||
import './App.css'
|
import './App.css'
|
||||||
import Toolbar from './components/Toolbar';
|
import Toolbar from './components/Toolbar'
|
||||||
import ControlPanel from './components/ControlPanel';
|
import ControlPanel from './components/ControlPanel'
|
||||||
import CustomBlock from './components/CustomBlock';
|
import CustomBlock from './components/CustomBlock'
|
||||||
import ModalData from './components/ModalData'
|
import ModalData from './components/ModalData'
|
||||||
|
import { convertGraphToXml, parseXmlToGraph } from './utils/xmlUtils'
|
||||||
|
|
||||||
const nodeTypes = {
|
const nodeTypes = {
|
||||||
fbdBlock: CustomBlock
|
fbdBlock: CustomBlock
|
||||||
@ -153,63 +154,59 @@ function App() {
|
|||||||
return {currentNodes, currentEdges}
|
return {currentNodes, currentEdges}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onConnect = useCallback(
|
const onConnect = useCallback((params) => {
|
||||||
(params) => {
|
const sourceNode = nodes.find(node => node.id === params.source)
|
||||||
const sourceNode = nodes.find(node => node.id === params.source)
|
const sourceIndex = parseInt(params.sourceHandle.split('-')[1], 10)
|
||||||
const sourceIndex = parseInt(params.sourceHandle.split('-')[1], 10)
|
const sourceType = sourceNode?.data?.outputs[sourceIndex].replace('$/', '') || 'INT'
|
||||||
const sourceType = sourceNode?.data?.outputs[sourceIndex].replace('$/', '') || 'INT'
|
|
||||||
|
|
||||||
const newEdge = {
|
const newEdge = {
|
||||||
...params,
|
...params,
|
||||||
id: `e_${Date.now()}`,
|
id: `e_${Date.now()}`,
|
||||||
type: 'smoothstep',
|
type: 'smoothstep',
|
||||||
style: EDGE_STYLES[sourceType.toLowerCase().split('/')[0]] || EDGE_STYLES['bool'],
|
style: EDGE_STYLES[sourceType.toLowerCase().split('/')[0]] || EDGE_STYLES['bool'],
|
||||||
data: { type: sourceType.toLowerCase().split('/')[0] }
|
data: { type: sourceType.toLowerCase().split('/')[0] }
|
||||||
}
|
}
|
||||||
|
|
||||||
const allEdges = [...edges, newEdge]
|
const allEdges = [...edges, newEdge]
|
||||||
const { currentNodes, currentEdges } = updateGraphTypes(allEdges, [...nodes])
|
const { currentNodes, currentEdges } = updateGraphTypes(allEdges, [...nodes])
|
||||||
setNodes(currentNodes)
|
setNodes(currentNodes)
|
||||||
setEdges(currentEdges)
|
setEdges(currentEdges)
|
||||||
}, [nodes, setNodes, setEdges, edgeType]
|
}, [nodes, setNodes, setEdges, edgeType])
|
||||||
)
|
|
||||||
|
|
||||||
const onDragOver = useCallback((event) => {
|
const onDragOver = useCallback((event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.dataTransfer.dropEffect = 'move'
|
event.dataTransfer.dropEffect = 'move'
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const onDrop = useCallback(
|
const onDrop = useCallback((event) => {
|
||||||
(event) => {
|
event.preventDefault()
|
||||||
event.preventDefault()
|
if (!flowInstance)
|
||||||
if (!flowInstance)
|
return
|
||||||
return
|
|
||||||
|
|
||||||
const dataStr = event.dataTransfer.getData('application/reactflow')
|
const dataStr = event.dataTransfer.getData('application/reactflow')
|
||||||
if (!dataStr)
|
if (!dataStr)
|
||||||
return
|
return
|
||||||
|
|
||||||
const {type, label, width, height, inputs = [], outputs = [],
|
const {type, label, width, height, inputs = [], outputs = [],
|
||||||
inputsLabels = [], outputsLabels = [], offsetX = 0, offsetY = 0} = JSON.parse(dataStr)
|
inputsLabels = [], outputsLabels = [], offsetX = 0, offsetY = 0} = JSON.parse(dataStr)
|
||||||
const position = flowInstance.screenToFlowPosition({
|
const position = flowInstance.screenToFlowPosition({
|
||||||
x: event.clientX - offsetX,
|
x: event.clientX - offsetX,
|
||||||
y: event.clientY - offsetY
|
y: event.clientY - offsetY
|
||||||
})
|
})
|
||||||
|
|
||||||
if (['const', 'switch_type', 'input', 'output'].includes(type)) {
|
if (['const', 'switch_type', 'input', 'output'].includes(type)) {
|
||||||
setModalData({ type, label, width, height, position, inputs, outputs, inputsLabels, outputsLabels })
|
setModalData({ type, label, width: widthPrew, height: heightPrew, position, inputs, outputs, inputsLabels, outputsLabels })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const newNode = {
|
const newNode = {
|
||||||
id: `${Date.now()}`,
|
id: `${Date.now()}`,
|
||||||
type: 'fbdBlock',
|
type: 'fbdBlock',
|
||||||
position,
|
position,
|
||||||
data: { type, label, width, height, inputs, outputs, inputsLabels, outputsLabels }
|
data: { type, label, width, height, inputs, outputs, inputsLabels, outputsLabels }
|
||||||
}
|
}
|
||||||
setNodes((currentNodes) => [...currentNodes, newNode])
|
setNodes((currentNodes) => [...currentNodes, newNode])
|
||||||
}, [flowInstance, setNodes]
|
}, [flowInstance, setNodes])
|
||||||
)
|
|
||||||
|
|
||||||
const onNodesDelete = useCallback((deletedNodes) => {
|
const onNodesDelete = useCallback((deletedNodes) => {
|
||||||
setNodes((nds) => nds.filter((node) => !deletedNodes.some((dn) => dn.id === node.id)))
|
setNodes((nds) => nds.filter((node) => !deletedNodes.some((dn) => dn.id === node.id)))
|
||||||
@ -299,6 +296,9 @@ function App() {
|
|||||||
if (!sourceNode || !targetNode)
|
if (!sourceNode || !targetNode)
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
if (edges.find(edge => edge.target === targetNode))
|
||||||
|
return false
|
||||||
|
|
||||||
const sourceIndex = parseInt(connection.sourceHandle.split('-')[1], 10)
|
const sourceIndex = parseInt(connection.sourceHandle.split('-')[1], 10)
|
||||||
const targetIndex = parseInt(connection.targetHandle.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')
|
const reactFlowViewport = document.querySelector('.react-flow__viewport')
|
||||||
if (!reactFlowViewport || !nodes.length > 0) {
|
if (!reactFlowViewport || nodes.length === 0) {
|
||||||
alert('Не удалось найти холст для экспорта')
|
alert('Не удалось найти холст для экспорта')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -370,227 +370,78 @@ function App() {
|
|||||||
console.error('Ошибка генерации SVG-файла: ', error)
|
console.error('Ошибка генерации SVG-файла: ', error)
|
||||||
alert('Не удалось сгененировать SVG-файл')
|
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) {
|
if (nodes.length === 0) {
|
||||||
alert('На холсте нет блоков для экспорта')
|
alert('На холсте нет блоков для экспорта')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let xml = `<?xml version="1.0" encoding="utf-8"?>\n`
|
const xml = convertGraphToXml(nodes, edges)
|
||||||
xml += `<project xmlns="http://www.plcopen.org/xml/tc6_0201" `
|
|
||||||
xml += `xmlns:xsd="http://www.w3.org/2001/XMLSchema-instance" `
|
|
||||||
xml += `xmlns:ns1="http://www.plcopen.org/xml/tc6_0201" `
|
|
||||||
xml += `xmlns:xhtml="http://www.w3.org/1999/xhtml\">\n`
|
|
||||||
|
|
||||||
xml += ` <fileHeader companyName="University" productName="FBD Editor" productVersion="1" creationDateTime="${new Date().toISOString().split('.')[0] + 'Z'}"/>\n`
|
|
||||||
xml += ` <contentHeader name="diagram" modificationDateTime="${new Date().toISOString().split('.')[0] + 'Z'}">\n`
|
|
||||||
xml += ` <coordinateInfo>\n`
|
|
||||||
xml += ` <fbd><scaling x="1" y="1"/></fbd>\n`
|
|
||||||
xml += ` <ld><scaling x="1" y="1"/></ld>\n`
|
|
||||||
xml += ` <sfc><scaling x="1" y="1"/></sfc>\n`
|
|
||||||
xml += ` </coordinateInfo>\n`
|
|
||||||
xml += ` </contentHeader>\n`
|
|
||||||
xml += ` <types>\n`
|
|
||||||
xml += ` <dataTypes/>\n`
|
|
||||||
xml += ` <pous>\n`
|
|
||||||
xml += ` <pou name="main" pouType="program">\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 += ` <interface>\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 += ` <inputVars>\n`
|
|
||||||
valuesIn.forEach((type, value) => {
|
|
||||||
xml += ` <variable name="${value}">\n`
|
|
||||||
xml += ` <type><${getPortType(type)}/></type>\n`
|
|
||||||
xml += ` </variable>\n`
|
|
||||||
})
|
|
||||||
xml += ` </inputVars>\n`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (valuesOut) {
|
|
||||||
xml += ` <outputVars>\n`
|
|
||||||
valuesOut.forEach((type, value) => {
|
|
||||||
xml += ` <variable name="${value}">\n`
|
|
||||||
xml += ` <type><${getPortType(type)}/></type>\n`
|
|
||||||
xml += ` </variable>\n`
|
|
||||||
})
|
|
||||||
xml += ` </outputVars>\n`
|
|
||||||
}
|
|
||||||
|
|
||||||
xml += ` </interface>\n`
|
|
||||||
|
|
||||||
xml += ` <body>\n`
|
|
||||||
xml += ` <FBD>\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 += ` <inVariable localId="${newInd[i]}" width="${node.data.width}" height="${node.data.height}">\n`
|
|
||||||
xml += ` <position x="${Math.round(node.position.x)}" y="${Math.round(node.position.y)}"/>\n`
|
|
||||||
xml += ` <connectionPointOut/>\n`
|
|
||||||
xml += ` <expression>${node.data.value}</expression>\n`
|
|
||||||
xml += ` </inVariable>\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 += ` <outVariable localId="${newInd[i]}" width="${node.data.width}" height="${node.data.height}">\n`
|
|
||||||
xml += ` <position x="${Math.round(node.position.x)}" y="${Math.round(node.position.y)}"/>\n`
|
|
||||||
xml += ` <connectionPointIn>\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 += ` <connection refLocalId="${getNewBlockId(edge.source)}"/>\n`
|
|
||||||
else {
|
|
||||||
const sourceIndex = parseInt(edge.sourceHandle.split('-')[1], 10)
|
|
||||||
const sourceLabel = sourceNode.data.outputsLabels[sourceIndex]
|
|
||||||
xml += ` <connection refLocalId="${getNewBlockId(edge.source)}" formalParameter="${sourceLabel}"/>\n`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
xml += ` </connectionPointIn>\n`
|
|
||||||
xml += ` <expression>${node.data.value}</expression>\n`
|
|
||||||
xml += ` </outVariable>\n\n`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
nodes.forEach(node => {
|
|
||||||
if (!['output', 'input', 'const'].includes(node.data.type)) {
|
|
||||||
xml += ` <block localId="${getNewBlockId(node.id)}" typeName="${node.data.type === 'switch_type' ? node.data.label.toUpperCase() : node.data.type.toUpperCase()}" width="${node.data.width}" height="${node.data.height}">\n`
|
|
||||||
xml += ` <position x="${Math.round(node.position.x)}" y="${Math.round(node.position.y)}"/>\n`
|
|
||||||
xml += ` <inputVariables>\n`
|
|
||||||
for (const portType of node.data.inputsLabels) {
|
|
||||||
xml += ` <variable formalParameter="${portType}">\n`
|
|
||||||
xml += ` <connectionPointIn>\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 += ` <connection refLocalId="${getNewBlockId(edge.source)}"/>\n`
|
|
||||||
else {
|
|
||||||
const sourceIndex = parseInt(edge.sourceHandle.split('-')[1], 10)
|
|
||||||
const sourceLabel = sourceNode.data.outputsLabels[sourceIndex]
|
|
||||||
xml += ` <connection refLocalId="${getNewBlockId(edge.source)}" formalParameter="${sourceLabel}"/>\n`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
xml += ` </connectionPointIn>\n`
|
|
||||||
xml += ` </variable>\n`
|
|
||||||
}
|
|
||||||
xml += ` </inputVariables>\n`
|
|
||||||
xml += ` <inOutVariables/>\n`
|
|
||||||
xml += ` <outputVariables>\n`
|
|
||||||
for (const portType of node.data.outputsLabels) {
|
|
||||||
xml += ` <variable formalParameter="${portType}">\n`
|
|
||||||
xml += ` <connectionPointOut/>\n`
|
|
||||||
xml += ` </variable>\n`
|
|
||||||
}
|
|
||||||
xml += ` </outputVariables>\n`
|
|
||||||
xml += ` </block>\n\n`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
xml += ` </FBD>\n`
|
|
||||||
xml += ` </body>\n`
|
|
||||||
xml += ` </pou>\n`
|
|
||||||
xml += ` </pous>\n`
|
|
||||||
xml += ` </types>\n`
|
|
||||||
xml += ` <instances>\n`
|
|
||||||
xml += ` <configurations/>\n`
|
|
||||||
xml += ` </instances>\n`
|
|
||||||
xml += `</project>`
|
|
||||||
|
|
||||||
const file = new Blob([xml], {type: 'application/xml'})
|
const file = new Blob([xml], {type: 'application/xml'})
|
||||||
|
|
||||||
if ('showSaveFilePicker' in window) {
|
if ('showSaveFilePicker' in window) {
|
||||||
try {
|
try {
|
||||||
const handle = await window.showSaveFilePicker({
|
const handle = await window.showSaveFilePicker({
|
||||||
@ -616,9 +467,9 @@ function App() {
|
|||||||
link.click()
|
link.click()
|
||||||
URL.revokeObjectURL(link.href)
|
URL.revokeObjectURL(link.href)
|
||||||
}
|
}
|
||||||
}, [nodes, edges])
|
}
|
||||||
|
|
||||||
const handleImportXml = useCallback((event) => {
|
function handleImportXml(event) {
|
||||||
const file = event.target.files[0]
|
const file = event.target.files[0]
|
||||||
if (!file)
|
if (!file)
|
||||||
return
|
return
|
||||||
@ -633,289 +484,7 @@ function App() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const importedNodes = []
|
const { finalNodes, finalEdges } = parseXmlToGraph(xml)
|
||||||
const importedEdges = []
|
|
||||||
const xmlIdToReactId = new Map()
|
|
||||||
const interfaceVars = new Map() // name: { dataType, initialValue, varGroup }
|
|
||||||
|
|
||||||
const xmlVariables = xml.querySelectorAll('interface * > variable')
|
|
||||||
xmlVariables.forEach(v => {
|
|
||||||
const name = v.getAttribute('name')
|
|
||||||
const varType = v.parentElement.tagName.toLowerCase()
|
|
||||||
const type = v.querySelector('type *')
|
|
||||||
const value = v.querySelector('initialValue simpleValue')?.getAttribute('value') || ''
|
|
||||||
if (name && type)
|
|
||||||
interfaceVars.set(name, {
|
|
||||||
dataType: type.tagName.toUpperCase(),
|
|
||||||
value,
|
|
||||||
varType
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
function restoreType(recivedType) {
|
|
||||||
const type = recivedType.toUpperCase()
|
|
||||||
if (type === 'ANY') return 'BOOl/INT/DINT/REAL/TIME/STRING'
|
|
||||||
if (type === 'ANY_MAGNITUDE') return 'INT/DINT/REAL/TIME'
|
|
||||||
if (type === 'ANY_NUM') return 'INT/DINT/REAL'
|
|
||||||
if (type === 'ANY_INT') return 'INT/DINT'
|
|
||||||
return type
|
|
||||||
}
|
|
||||||
|
|
||||||
const xmlInVariables = xml.querySelectorAll('inVariable')
|
|
||||||
xmlInVariables.forEach(inVar => {
|
|
||||||
const xmlId = inVar.getAttribute('localId')
|
|
||||||
const width = parseFloat(inVar.getAttribute('width') || '80')
|
|
||||||
const height = parseFloat(inVar.getAttribute('height') || '40')
|
|
||||||
const pos = inVar.querySelector('position')
|
|
||||||
const x = parseFloat(pos?.getAttribute('x') || '0')
|
|
||||||
const y = parseFloat(pos?.getAttribute('y') || '0')
|
|
||||||
const value = inVar.querySelector('expression')?.textContent || ''
|
|
||||||
|
|
||||||
function isItConst(recivedValue) {
|
|
||||||
const value = recivedValue.toLowerCase().trim()
|
|
||||||
const timeRegex = /^(t|time)#(-)?\d+(d|h|m|s|ms)(\d+(d|h|m|s|ms))*/
|
|
||||||
const realRegex = /^-?\d+\.\d+$/
|
|
||||||
const intRegex = /^-?\d+$/
|
|
||||||
|
|
||||||
if (value === 'true' || value === 'false')
|
|
||||||
return 'BOOL'
|
|
||||||
if (timeRegex.test(value))
|
|
||||||
return 'TIME'
|
|
||||||
if (value.startsWith("'") && value.endsWith("'"))
|
|
||||||
return 'STRING'
|
|
||||||
if (realRegex.test(value))
|
|
||||||
return 'REAL'
|
|
||||||
if (intRegex.test(value)) {
|
|
||||||
const num = Number(value)
|
|
||||||
if (num >= -32768 && num <= 32767)
|
|
||||||
return 'INT'
|
|
||||||
if (num >= -2147483648 && num <= 2147483647)
|
|
||||||
return 'DINT'
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
let isConst = isItConst(value) ? true : false
|
|
||||||
let varInfo = interfaceVars.get(value)
|
|
||||||
const outputPortType = varInfo?.dataType || isItConst(value) || 'ANY'
|
|
||||||
|
|
||||||
if (varInfo?.varType === 'tempvars')
|
|
||||||
isConst = true
|
|
||||||
|
|
||||||
const finalType = isConst ? 'const' : 'input'
|
|
||||||
let finalValue = value
|
|
||||||
if (varInfo?.varType === 'tempvars' && varInfo.value)
|
|
||||||
finalValue = varInfo.value
|
|
||||||
|
|
||||||
let existingNode = importedNodes.find(node => node.position.x === x && node.position.y === y && node.data.value === finalValue)
|
|
||||||
if (!existingNode) {
|
|
||||||
const newId = `${xmlId}`
|
|
||||||
const label = finalType === 'const' ? `${finalValue} (${outputPortType})` : `${finalValue} (${outputPortType})`
|
|
||||||
existingNode = {
|
|
||||||
id: newId,
|
|
||||||
type: 'fbdBlock',
|
|
||||||
position: {x, y},
|
|
||||||
data: {
|
|
||||||
type: finalType, label,
|
|
||||||
value: finalValue,
|
|
||||||
metaFromType: outputPortType,
|
|
||||||
width, height,
|
|
||||||
inputs: [],
|
|
||||||
outputs: [restoreType(outputPortType)],
|
|
||||||
inputsLabels: [],
|
|
||||||
outputsLabels: ['OUT']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
importedNodes.push(existingNode)
|
|
||||||
}
|
|
||||||
xmlIdToReactId.set(xmlId, {reactId: existingNode.id, portIndex: 0, handleType: 'out'})
|
|
||||||
})
|
|
||||||
|
|
||||||
const xmlOutVariables = xml.querySelectorAll('outVariable')
|
|
||||||
xmlOutVariables.forEach(outVar => {
|
|
||||||
const xmlId = outVar.getAttribute('localId')
|
|
||||||
const width = parseFloat(outVar.getAttribute('width') || '80')
|
|
||||||
const height = parseFloat(outVar.getAttribute('height') || '40')
|
|
||||||
const pos = outVar.querySelector('position')
|
|
||||||
const x = parseFloat(pos?.getAttribute('x') || '0')
|
|
||||||
const y = parseFloat(pos?.getAttribute('y') || '0')
|
|
||||||
const value = outVar.querySelector('expression')?.textContent || ''
|
|
||||||
let varInfo = interfaceVars.get(value)
|
|
||||||
|
|
||||||
const dataType = varInfo?.dataType || 'ANY'
|
|
||||||
let existingNode = importedNodes.find(node => node.position.x === x && node.position.y === y && node.data.value === value)
|
|
||||||
if (!existingNode) {
|
|
||||||
existingNode = {
|
|
||||||
id: xmlId,
|
|
||||||
type: 'fbdBlock',
|
|
||||||
position: {x, y},
|
|
||||||
data: {
|
|
||||||
type: 'output',
|
|
||||||
label: `${value} (${dataType})`,
|
|
||||||
value,
|
|
||||||
metaFromType: dataType,
|
|
||||||
width, height,
|
|
||||||
inputs: [restoreType(dataType)],
|
|
||||||
outputs: [],
|
|
||||||
inputsLabels: ['IN'],
|
|
||||||
outputsLabels: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
importedNodes.push(existingNode)
|
|
||||||
}
|
|
||||||
xmlIdToReactId.set(xmlId, {reactId: existingNode.id, portIndex: 0, handleType: 'in'})
|
|
||||||
|
|
||||||
const connection = outVar.querySelector('connectionPointIn connection')
|
|
||||||
if (connection) {
|
|
||||||
const sourceNode = connection.getAttribute('refLocalId')
|
|
||||||
const sourcePortLabel = connection.getAttribute('formalParameter')
|
|
||||||
importedEdges.push({
|
|
||||||
xmlSourceNode: sourceNode,
|
|
||||||
xmlSourcePortLabel: sourcePortLabel,
|
|
||||||
reactTargetId: existingNode.id,
|
|
||||||
reactTargetPort: 'in-0'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const libraryBlocks = {
|
|
||||||
and: { label: 'AND', inputs: ['BOOL', 'BOOL'], outputs: ['BOOL'], inputsLabels: ['IN1', 'IN2'], outputsLabels: ['OUT'] },
|
|
||||||
or: { label: 'OR', inputs: ['BOOL', 'BOOL'], outputs: ['BOOL'], inputsLabels: ['IN1', 'IN2'], outputsLabels: ['OUT'] },
|
|
||||||
not: { label: 'NOT', inputs: ['BOOL'], outputs: ['BOOL'], inputsLabels: ['IN'], outputsLabels: ['OUT'] },
|
|
||||||
xor: { label: 'XOR', inputs: ['BOOL', 'BOOL'], outputs: ['BOOL'], inputsLabels: ['IN1', 'IN2'], outputsLabels: ['OUT'] },
|
|
||||||
add: { label: 'ADD', inputs: ['$/INT/DINT/REAL', '$/INT/DINT/REAL'], outputs: ['$/INT/DINT/REAL'], inputsLabels: ['IN1', 'IN2'], outputsLabels: ['OUT'] },
|
|
||||||
sub: { label: 'SUB', inputs: ['$/INT/DINT/REAL', '$/INT/DINT/REAL'], outputs: ['$/INT/DINT/REAL'], inputsLabels: ['IN1', 'IN2'], outputsLabels: ['OUT'] },
|
|
||||||
mul: { label: 'MUL', inputs: ['$/INT/DINT/REAL', '$/INT/DINT/REAL'], outputs: ['$/INT/DINT/REAL'], inputsLabels: ['IN1', 'IN2'], outputsLabels: ['OUT'] },
|
|
||||||
div: { label: 'DIV', inputs: ['$/INT/DINT/REAL', '$/INT/DINT/REAL'], outputs: ['$/INT/DINT/REAL'], inputsLabels: ['IN1', 'IN2'], outputsLabels: ['OUT'] },
|
|
||||||
ton: { label: 'TON', inputs: ['BOOL', 'TIME'], outputs: ['BOOL', 'TIME'], inputsLabels: ['IN', 'PT'], outputsLabels: ['Q', 'ET'] },
|
|
||||||
tof: { label: 'TOF', inputs: ['BOOL', 'TIME'], outputs: ['BOOL', 'TIME'], inputsLabels: ['IN', 'PT'], outputsLabels: ['Q', 'ET'] },
|
|
||||||
tp: { label: 'TP', inputs: ['BOOL', 'TIME'], outputs: ['BOOL', 'TIME'], inputsLabels: ['IN', 'PT'], outputsLabels: ['Q', 'ET'] },
|
|
||||||
ctu: { label: 'CTU', inputs: ['BOOL', 'BOOL', '$/INT/DINT'], outputs: ['BOOL', '$/INT/DINT'], inputsLabels: ['CU', 'R', 'PV'], outputsLabels: ['Q', 'CV'] },
|
|
||||||
ctd: { label: 'CTD', inputs: ['BOOL', 'BOOL', '$/INT/DINT'], outputs: ['BOOL', '$/INT/DINT'], inputsLabels: ['CD', 'LD', 'PV'], outputsLabels: ['Q', 'CV'] },
|
|
||||||
ctud: { label: 'CTUD', inputs: ['BOOL', 'BOOL', 'BOOL', 'BOOL', '$/INT/DINT'], outputs: ['BOOL', 'BOOL', '$/INT/DINT'], inputsLabels: ['CU', 'CD', 'R', 'LD', 'PV'], outputsLabels: ['QU', 'QD', 'CV'] },
|
|
||||||
gt: { label: '>', inputs: ['$/INT/DINT/REAL/TIME', '$/INT/DINT/REAL/TIME'], outputs: ['BOOL'], inputsLabels: ['IN1', 'IN2'], outputsLabels: ['OUT'] },
|
|
||||||
lt: { label: '<', inputs: ['$/INT/DINT/REAL/TIME', '$/INT/DINT/REAL/TIME'], outputs: ['BOOL'], inputsLabels: ['IN1', 'IN2'], outputsLabels: ['OUT'] },
|
|
||||||
eq: { label: '=', inputs: ['$/BOOl/INT/DINT/REAL/TIME/STRING', '$/BOOl/INT/DINT/REAL/TIME/STRING'], outputs: ['BOOL'], inputsLabels: ['IN1', 'IN2'], outputsLabels: ['OUT'] },
|
|
||||||
sr: { label: 'SR', inputs: ['BOOL', 'BOOL'], outputs: ['BOOL'], inputsLabels: ['S1', 'R'], outputsLabels: ['Q1'] },
|
|
||||||
rs: { label: 'RS', inputs: ['BOOL', 'BOOL'], outputs: ['BOOL'], inputsLabels: ['S', 'R1'], outputsLabels: ['Q'] },
|
|
||||||
move: { label: 'MOVE', inputs: ['$/BOOl/INT/DINT/REAL/TIME/STRING'], outputs: ['$/BOOl/INT/DINT/REAL/TIME/STRING'], inputsLabels: ['IN'], outputsLabels: ['OUT'] },
|
|
||||||
switch_type: { label: '*_TO_**', inputs: ['BOOl/INT/DINT/REAL/TIME/STRING'], outputs: ['BOOl/INT/DINT/REAL/TIME/STRING'], inputsLabels: ['IN'], outputsLabels: ['OUT'] }
|
|
||||||
}
|
|
||||||
|
|
||||||
const xmlBlocks = xml.querySelectorAll('FBD > block')
|
|
||||||
xmlBlocks.forEach(block => {
|
|
||||||
const xmlId = block.getAttribute('localId')
|
|
||||||
const xmlType = block.getAttribute('typeName')
|
|
||||||
const pos = block.querySelector('position')
|
|
||||||
const x = parseFloat(pos?.getAttribute('x') || '0')
|
|
||||||
const y = parseFloat(pos?.getAttribute('y') || '0')
|
|
||||||
const label = block.querySelector('content')?.textContent || libraryBlocks[xmlType.toLowerCase()]?.label || xmlType
|
|
||||||
const value = block.querySelector('additionalData')?.getAttribute('value') || ''
|
|
||||||
let inputsLabels = Array.from(block.querySelectorAll('inputVariables variable')).map(v => v.getAttribute('formalParameter'))
|
|
||||||
let inputs = Array.from(block.querySelectorAll('inputVariables variable')).map(v => restoreType(v.getAttribute('dataType') || 'ANY'))
|
|
||||||
let outputsLabels = Array.from(block.querySelectorAll('outputVariables variable')).map(v => v.getAttribute('formalParameter'))
|
|
||||||
let outputs = Array.from(block.querySelectorAll('outputVariables variable')).map(v => restoreType(v.getAttribute('dataType') || 'ANY'))
|
|
||||||
|
|
||||||
if (inputsLabels.length === libraryBlocks[xmlType.toLowerCase()]?.inputsLabels.length)
|
|
||||||
inputsLabels = libraryBlocks[xmlType.toLowerCase()].inputsLabels
|
|
||||||
if (outputsLabels.length === libraryBlocks[xmlType.toLowerCase()]?.outputsLabels.length)
|
|
||||||
outputsLabels = libraryBlocks[xmlType.toLowerCase()].outputsLabels
|
|
||||||
if (inputs.length === libraryBlocks[xmlType.toLowerCase()]?.inputs.length)
|
|
||||||
inputs = libraryBlocks[xmlType.toLowerCase()].inputs
|
|
||||||
if (outputs.length === libraryBlocks[xmlType.toLowerCase()]?.outputs.length)
|
|
||||||
outputs = libraryBlocks[xmlType.toLowerCase()].outputs
|
|
||||||
|
|
||||||
const isSwitchType = xmlType.includes('_TO_')
|
|
||||||
const finalType = isSwitchType ? 'switch_type' : xmlType.toLowerCase()
|
|
||||||
const width = parseFloat(block.getAttribute('width') || (isSwitchType ? '80' : 50 + (Math.max(inputs.length, outputs.length, 2) - 1) * 34))
|
|
||||||
const height = parseFloat(block.getAttribute('height') || (isSwitchType ? '40' : '60'))
|
|
||||||
const newNode = {
|
|
||||||
id: xmlId,
|
|
||||||
type: 'fbdBlock',
|
|
||||||
position: {x, y},
|
|
||||||
data: { type: finalType, label, value, width, height, inputs, outputs, inputsLabels, outputsLabels}
|
|
||||||
}
|
|
||||||
importedNodes.push(newNode)
|
|
||||||
xmlIdToReactId.set(xmlId, { reactId: xmlId, inputsLabels, outputsLabels })
|
|
||||||
|
|
||||||
const variable = block.querySelectorAll('inputVariables variable')
|
|
||||||
variable.forEach((v, inputIndex) => {
|
|
||||||
const connection = v.querySelector('connectionPointIn connection')
|
|
||||||
if (connection) {
|
|
||||||
const sourceNode = connection.getAttribute('refLocalId')
|
|
||||||
const sourcePortLabel = connection.getAttribute('formalParameter')
|
|
||||||
importedEdges.push({
|
|
||||||
xmlSourceNode: sourceNode,
|
|
||||||
xmlSourcePortLabel: sourcePortLabel,
|
|
||||||
reactTargetId: xmlId,
|
|
||||||
reactTargetPort: `in-${inputIndex}`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const finalEdges = importedEdges.map((edgeData, index) => {
|
|
||||||
const sourceMeta = xmlIdToReactId.get(edgeData.xmlSourceNode)
|
|
||||||
if (!sourceMeta)
|
|
||||||
return null
|
|
||||||
let sourceHandle = 'out-0'
|
|
||||||
if (sourceMeta.outputsLabels) {
|
|
||||||
const outIndex = sourceMeta.outputsLabels.indexOf(edgeData.xmlSourcePortLabel)
|
|
||||||
sourceHandle = `out-${outIndex >= 0 ? outIndex : 0}`
|
|
||||||
}
|
|
||||||
const sourceNode = importedNodes.find(node => node.id === sourceMeta.reactId)
|
|
||||||
const sourceIndex = parseInt(sourceHandle.split('-')[1], 10)
|
|
||||||
const sourceType = sourceNode?.data?.outputs[sourceIndex] || 'ANY'
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: `e_${Date.now()}_${index}`,
|
|
||||||
source: sourceMeta.reactId,
|
|
||||||
target: edgeData.reactTargetId,
|
|
||||||
sourceHandle: sourceHandle,
|
|
||||||
targetHandle: edgeData.reactTargetPort,
|
|
||||||
type: 'smoothstep',
|
|
||||||
style: EDGE_STYLES[sourceType.toLowerCase().split('/')[0]] || EDGE_STYLES['bool'],
|
|
||||||
data: {type: sourceType.toLowerCase().split('/')[0]}
|
|
||||||
}
|
|
||||||
}).filter(Boolean)
|
|
||||||
|
|
||||||
const finalNodes = importedNodes.map(node => {
|
|
||||||
if (['add', 'sub', 'mul', 'div', 'move'].includes(node.data.type))
|
|
||||||
return {
|
|
||||||
...node,
|
|
||||||
data: {
|
|
||||||
...node.data,
|
|
||||||
inputs: node.data.inputs.map(i => '$/' + i),
|
|
||||||
outputs: node.data.outputs.map(i => '$/' + i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (['ctu', 'ctd', 'ctud'].includes(node.data.type))
|
|
||||||
return {
|
|
||||||
...node,
|
|
||||||
data: {
|
|
||||||
...node.data,
|
|
||||||
inputs: node.data.inputs.map((i, ind) => {
|
|
||||||
if (node.data.inputsLabels.toLowerCase() === 'pv')
|
|
||||||
return '$/' + i
|
|
||||||
else return i
|
|
||||||
}),
|
|
||||||
outputs: node.data.outputs.map((i, ind) => {
|
|
||||||
if (node.data.inputsLabels.toLowerCase() === 'cv')
|
|
||||||
return '$/' + i
|
|
||||||
else return i
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (['gt', 'lt', 'eq'].includes(node.data.type))
|
|
||||||
return {
|
|
||||||
...node,
|
|
||||||
data: {
|
|
||||||
...node.data,
|
|
||||||
inputs: node.data.inputs.map(i => '$/' + i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else return node
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { currentNodes, currentEdges } = updateGraphTypes(finalEdges, finalNodes)
|
const { currentNodes, currentEdges } = updateGraphTypes(finalEdges, finalNodes)
|
||||||
@ -930,13 +499,14 @@ function App() {
|
|||||||
}
|
}
|
||||||
reader.readAsText(file)
|
reader.readAsText(file)
|
||||||
event.target.value = ''
|
event.target.value = ''
|
||||||
}, [nodes, edges])
|
}
|
||||||
|
|
||||||
return(
|
return(
|
||||||
<div className="App">
|
<div className="App">
|
||||||
<ControlPanel
|
<ControlPanel
|
||||||
onClear = {handleClear}
|
onClear = {handleClear}
|
||||||
onExportSvg = {handleExportSvg}
|
onExportSvg = {handleExportSvg}
|
||||||
|
onExportPng = {handleExportPng}
|
||||||
onExportXml = {handleExportXml}
|
onExportXml = {handleExportXml}
|
||||||
onImportXml = {handleImportXml}
|
onImportXml = {handleImportXml}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React, {useRef} from 'react';
|
import React, {useRef} from 'react';
|
||||||
|
|
||||||
function ControlPanel({ onClear, onExportSvg, onExportXml, onImportXml }) {
|
function ControlPanel({ onClear, onExportSvg, onExportPng, onExportXml, onImportXml }) {
|
||||||
const fileInputRef = useRef(null)
|
const fileInputRef = useRef(null)
|
||||||
|
|
||||||
const handleImportButtonClick = () => {
|
const handleImportButtonClick = () => {
|
||||||
@ -16,6 +16,12 @@ function ControlPanel({ onClear, onExportSvg, onExportXml, onImportXml }) {
|
|||||||
>
|
>
|
||||||
Экспорт в SVG
|
Экспорт в SVG
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick = {onExportPng}
|
||||||
|
className = "control-button svg-export-button"
|
||||||
|
>
|
||||||
|
Экспорт в PNG
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick = {onExportXml}
|
onClick = {onExportXml}
|
||||||
className = "control-button svg-export-button"
|
className = "control-button svg-export-button"
|
||||||
|
|||||||
@ -1,19 +1,27 @@
|
|||||||
import React, { memo } from 'react';
|
import React, { memo } from 'react'
|
||||||
import { Handle, Position, useEdges } from '@xyflow/react';
|
import { Handle, Position, useEdges } from '@xyflow/react'
|
||||||
|
|
||||||
function CustomBlock({ id: blockId, data }) {
|
function CustomBlock({ id: blockId, data }) {
|
||||||
const {type, label, inputs = [], outputs = [], metaFromType, metaToType } = data
|
const {type, label, inputs = [], outputs = [], inputsLabels = [], outputsLabels = [], metaFromType, metaToType } = data
|
||||||
const edges = useEdges()
|
const edges = useEdges()
|
||||||
|
|
||||||
const inputCount = inputs.length
|
const inputCount = inputs.length
|
||||||
const outputCount = outputs.length
|
const outputCount = outputs.length
|
||||||
|
let blockWidth = data.width || 60
|
||||||
|
let blockHeight = data.height
|
||||||
let countForHeight = Math.max(inputCount, outputCount, 2)
|
let countForHeight = Math.max(inputCount, outputCount, 2)
|
||||||
let blockHeight = 50 + (countForHeight - 1) * 34
|
|
||||||
let blockWidth = 60
|
if (!blockHeight)
|
||||||
if (['input', 'output', 'const', 'switch_type'].includes(type)) {
|
blockHeight = 50 + (countForHeight - 1) * 34
|
||||||
|
if (['input', 'output', 'const'].includes(type)) {
|
||||||
countForHeight = Math.max(inputCount, outputCount)
|
countForHeight = Math.max(inputCount, outputCount)
|
||||||
blockHeight = 40 + (countForHeight - 1) * 34
|
blockHeight = data.height || 40 + (countForHeight - 1) * 34
|
||||||
blockWidth = blockHeight * 2
|
blockWidth = data.width || blockHeight * 2.1
|
||||||
|
}
|
||||||
|
if (type === 'switch_type') {
|
||||||
|
countForHeight = Math.max(inputCount, outputCount)
|
||||||
|
blockHeight = data.height || 40 + (countForHeight - 1) * 34
|
||||||
|
blockWidth = data.width || blockHeight * 2.5
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPortStyle(portType, portIndex) {
|
function getPortStyle(portType, portIndex) {
|
||||||
@ -64,25 +72,30 @@ function CustomBlock({ id: blockId, data }) {
|
|||||||
const outputsCoords = getCoords(outputCount)
|
const outputsCoords = getCoords(outputCount)
|
||||||
|
|
||||||
function getAddElements() {
|
function getAddElements() {
|
||||||
if (['ton', 'tof', 'tp'].includes(type))
|
if (!['and', 'or', 'not', 'xor', 'add', 'sub', 'mul', 'div', 'gt', 'lt', 'eq', 'move', 'input', 'output', 'const', 'switch_type'].includes(type))
|
||||||
return(
|
return(
|
||||||
<>
|
<>
|
||||||
<div className = "block-inputs-title" style = {{transform: `translate(0px, ${inputsCoords[0]}px)`}}>in</div>
|
{inputsLabels.map((inLab, ind) => {
|
||||||
<div className = "block-inputs-title" style = {{transform: `translate(0px, ${inputsCoords[1]}px)`}}>pt</div>
|
return (
|
||||||
<div className = "block-outputs-title" style = {{transform: `translate(0px, ${outputsCoords[0]}px)`}}>q</div>
|
<div className = "block-inputs-title" style = {{
|
||||||
<div className = "block-outputs-title" style = {{transform: `translate(0px, ${outputsCoords[1]}px)`}}>et</div>
|
top: `${inputsCoords[ind]}%`,
|
||||||
|
left: 3,
|
||||||
|
textAlign: 'left'
|
||||||
|
}}>{inLab}</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
{outputsLabels.map((outLab, ind) => {
|
||||||
|
return (
|
||||||
|
<div className = "block-outputs-title" style = {{
|
||||||
|
top: `${outputsCoords[ind]}%`,
|
||||||
|
left: `${blockWidth - 23}px`,
|
||||||
|
right: '0',
|
||||||
|
textAlign: 'right'
|
||||||
|
}}>{outLab}</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
if (type === 'ctu')
|
|
||||||
return {/*cu r pv, q cv*/}
|
|
||||||
if (type === 'ctd')
|
|
||||||
return {/*cd ld pv, q cv*/}
|
|
||||||
if (type === 'ctud')
|
|
||||||
return {/*cu cd r ld pv, qu qd cv*/}
|
|
||||||
if (type === 'rs')
|
|
||||||
return {/*s1 r, q1*/}
|
|
||||||
if (type === 'sr')
|
|
||||||
return {/*s r1, q*/}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -38,7 +38,7 @@ const BLOCK_CATEGORIES = {
|
|||||||
other: [
|
other: [
|
||||||
{ type: 'sr', label: 'SR', inputs: ['BOOL', 'BOOL'], outputs: ['BOOL'], inputsLabels: ['S1', 'R'], outputsLabels: ['Q1'] },
|
{ type: 'sr', label: 'SR', inputs: ['BOOL', 'BOOL'], outputs: ['BOOL'], inputsLabels: ['S1', 'R'], outputsLabels: ['Q1'] },
|
||||||
{ type: 'rs', label: 'RS', inputs: ['BOOL', 'BOOL'], outputs: ['BOOL'], inputsLabels: ['S', 'R1'], outputsLabels: ['Q'] },
|
{ type: 'rs', label: 'RS', inputs: ['BOOL', 'BOOL'], outputs: ['BOOL'], inputsLabels: ['S', 'R1'], outputsLabels: ['Q'] },
|
||||||
{ type: 'move', label: 'MOVE', inputs: ['$/BOOl/INT/DINT/REAL/TIME/STRING'], outputs: ['$/BOOl/INT/DINT/REAL/TIME/STRING'], inputsLabels: ['IN'], outputsLabels: ['OUT'] }],
|
{ type: 'move', label: 'MOVE', inputs: ['$/BOOl/INT/DINT/REAL/TIME/STRING'], outputs: ['$/BOOl/INT/DINT/REAL/TIME/STRING', 'BOOL'], inputsLabels: ['IN'], outputsLabels: ['OUT', 'EN'] }],
|
||||||
data: [
|
data: [
|
||||||
{ type: 'input', label: 'input', inputs: [], outputs: ['BOOl/INT/DINT/REAL/TIME/STRING'], inputsLabels: [], outputsLabels: ['OUT'] },
|
{ type: 'input', label: 'input', inputs: [], outputs: ['BOOl/INT/DINT/REAL/TIME/STRING'], inputsLabels: [], outputsLabels: ['OUT'] },
|
||||||
{ type: 'output', label: 'output', inputs: ['BOOl/INT/DINT/REAL/TIME/STRING'], outputs: [], inputsLabels: ['IN'], outputsLabels: [] },
|
{ type: 'output', label: 'output', inputs: ['BOOl/INT/DINT/REAL/TIME/STRING'], outputs: [], inputsLabels: ['IN'], outputsLabels: [] },
|
||||||
@ -91,8 +91,8 @@ function Toolbar() {
|
|||||||
event.dataTransfer.setData('application/reactflow', JSON.stringify({
|
event.dataTransfer.setData('application/reactflow', JSON.stringify({
|
||||||
type: block.type,
|
type: block.type,
|
||||||
label: block.label,
|
label: block.label,
|
||||||
width: blockWidth,
|
widthPrew: blockWidth,
|
||||||
height: blockHeight,
|
heightPrew: blockHeight,
|
||||||
inputs: block.inputs,
|
inputs: block.inputs,
|
||||||
outputs: block.outputs,
|
outputs: block.outputs,
|
||||||
inputsLabels: block.inputsLabels,
|
inputsLabels: block.inputsLabels,
|
||||||
|
|||||||
@ -11,8 +11,8 @@ export const ToolbarBlock = memo(({ block, onDragStart, scale = 1 }) => {
|
|||||||
blockHeight = (40 + (countForHeight - 1) * 34) / scale
|
blockHeight = (40 + (countForHeight - 1) * 34) / scale
|
||||||
blockWidth = blockHeight * 2
|
blockWidth = blockHeight * 2
|
||||||
}
|
}
|
||||||
block.width = blockWidth
|
block.widthPrew = blockWidth
|
||||||
block.height = blockHeight
|
block.heightPrew = blockHeight
|
||||||
|
|
||||||
function getCoords(count) {
|
function getCoords(count) {
|
||||||
const coords = []
|
const coords = []
|
||||||
|
|||||||
503
src/utils/xmlUtils.jsx
Normal file
503
src/utils/xmlUtils.jsx
Normal file
@ -0,0 +1,503 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export function convertGraphToXml(nodes, edges) {
|
||||||
|
let xml = `<?xml version="1.0" encoding="utf-8"?>\n`
|
||||||
|
xml += `<project xmlns="http://www.plcopen.org/xml/tc6_0201" `
|
||||||
|
xml += `xmlns:xsd="http://www.w3.org/2001/XMLSchema-instance" `
|
||||||
|
xml += `xmlns:ns1="http://www.plcopen.org/xml/tc6_0201" `
|
||||||
|
xml += `xmlns:xhtml="http://www.w3.org/1999/xhtml\">\n`
|
||||||
|
|
||||||
|
xml += ` <fileHeader companyName="University" productName="FBD Editor" productVersion="1" creationDateTime="${new Date().toISOString().split('.')[0] + 'Z'}"/>\n`
|
||||||
|
xml += ` <contentHeader name="diagram" modificationDateTime="${new Date().toISOString().split('.')[0] + 'Z'}">\n`
|
||||||
|
xml += ` <coordinateInfo>\n`
|
||||||
|
xml += ` <fbd><scaling x="1" y="1"/></fbd>\n`
|
||||||
|
xml += ` <ld><scaling x="1" y="1"/></ld>\n`
|
||||||
|
xml += ` <sfc><scaling x="1" y="1"/></sfc>\n`
|
||||||
|
xml += ` </coordinateInfo>\n`
|
||||||
|
xml += ` </contentHeader>\n`
|
||||||
|
xml += ` <types>\n`
|
||||||
|
xml += ` <dataTypes/>\n`
|
||||||
|
xml += ` <pous>\n`
|
||||||
|
xml += ` <pou name="main" pouType="program">\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 += ` <interface>\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 += ` <inputVars>\n`
|
||||||
|
valuesIn.forEach((type, value) => {
|
||||||
|
xml += ` <variable name="${value}">\n`
|
||||||
|
xml += ` <type><${getPortType(type)}/></type>\n`
|
||||||
|
xml += ` </variable>\n`
|
||||||
|
})
|
||||||
|
xml += ` </inputVars>\n`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valuesOut) {
|
||||||
|
xml += ` <outputVars>\n`
|
||||||
|
valuesOut.forEach((type, value) => {
|
||||||
|
xml += ` <variable name="${value}">\n`
|
||||||
|
xml += ` <type><${getPortType(type)}/></type>\n`
|
||||||
|
xml += ` </variable>\n`
|
||||||
|
})
|
||||||
|
xml += ` </outputVars>\n`
|
||||||
|
}
|
||||||
|
|
||||||
|
xml += ` </interface>\n`
|
||||||
|
|
||||||
|
xml += ` <body>\n`
|
||||||
|
xml += ` <FBD>\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 += ` <inVariable localId="${newInd[i]}" width="${node.data.width}" height="${node.data.height}">\n`
|
||||||
|
xml += ` <position x="${Math.round(node.position.x)}" y="${Math.round(node.position.y)}"/>\n`
|
||||||
|
xml += ` <connectionPointOut/>\n`
|
||||||
|
xml += ` <expression>${node.data.value}</expression>\n`
|
||||||
|
xml += ` </inVariable>\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 += ` <outVariable localId="${newInd[i]}" width="${node.data.width}" height="${node.data.height}">\n`
|
||||||
|
xml += ` <position x="${Math.round(node.position.x)}" y="${Math.round(node.position.y)}"/>\n`
|
||||||
|
xml += ` <connectionPointIn>\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 += ` <connection refLocalId="${getNewBlockId(edge.source)}"/>\n`
|
||||||
|
else {
|
||||||
|
const sourceIndex = parseInt(edge.sourceHandle.split('-')[1], 10)
|
||||||
|
const sourceLabel = sourceNode.data.outputsLabels[sourceIndex]
|
||||||
|
xml += ` <connection refLocalId="${getNewBlockId(edge.source)}" formalParameter="${sourceLabel}"/>\n`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
xml += ` </connectionPointIn>\n`
|
||||||
|
xml += ` <expression>${node.data.value}</expression>\n`
|
||||||
|
xml += ` </outVariable>\n\n`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
nodes.forEach(node => {
|
||||||
|
if (!['output', 'input', 'const'].includes(node.data.type)) {
|
||||||
|
xml += ` <block localId="${getNewBlockId(node.id)}" typeName="${node.data.type === 'switch_type' ? node.data.label.toUpperCase() : node.data.type.toUpperCase()}" width="${node.data.width}" height="${node.data.height}">\n`
|
||||||
|
xml += ` <position x="${Math.round(node.position.x)}" y="${Math.round(node.position.y)}"/>\n`
|
||||||
|
xml += ` <inputVariables>\n`
|
||||||
|
for (const portType of node.data.inputsLabels) {
|
||||||
|
xml += ` <variable formalParameter="${portType}">\n`
|
||||||
|
xml += ` <connectionPointIn>\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 += ` <connection refLocalId="${getNewBlockId(edge.source)}"/>\n`
|
||||||
|
else {
|
||||||
|
const sourceIndex = parseInt(edge.sourceHandle.split('-')[1], 10)
|
||||||
|
const sourceLabel = sourceNode.data.outputsLabels[sourceIndex]
|
||||||
|
xml += ` <connection refLocalId="${getNewBlockId(edge.source)}" formalParameter="${sourceLabel}"/>\n`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
xml += ` </connectionPointIn>\n`
|
||||||
|
xml += ` </variable>\n`
|
||||||
|
}
|
||||||
|
xml += ` </inputVariables>\n`
|
||||||
|
xml += ` <inOutVariables/>\n`
|
||||||
|
xml += ` <outputVariables>\n`
|
||||||
|
for (const portType of node.data.outputsLabels) {
|
||||||
|
xml += ` <variable formalParameter="${portType}">\n`
|
||||||
|
xml += ` <connectionPointOut/>\n`
|
||||||
|
xml += ` </variable>\n`
|
||||||
|
}
|
||||||
|
xml += ` </outputVariables>\n`
|
||||||
|
xml += ` </block>\n\n`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
xml += ` </FBD>\n`
|
||||||
|
xml += ` </body>\n`
|
||||||
|
xml += ` </pou>\n`
|
||||||
|
xml += ` </pous>\n`
|
||||||
|
xml += ` </types>\n`
|
||||||
|
xml += ` <instances>\n`
|
||||||
|
xml += ` <configurations/>\n`
|
||||||
|
xml += ` </instances>\n`
|
||||||
|
xml += `</project>`
|
||||||
|
return xml
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseXmlToGraph(xml) {
|
||||||
|
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
|
||||||
|
})
|
||||||
|
return {finalNodes, finalEdges}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user