168 lines
5.4 KiB
JavaScript
168 lines
5.4 KiB
JavaScript
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 '@xyflow/react/dist/style.css'
|
||
import './App.css'
|
||
import Toolbar from './components/Toolbar';
|
||
import ControlPanel from './components/ControlPanel';
|
||
import CustomBlock from './components/CustomBlock';
|
||
|
||
const nodeTypes = {
|
||
fbdBlock: CustomBlock
|
||
}
|
||
|
||
const EDGE_STYLES = {
|
||
bool: { stroke: '#000', strokeWidth: 2 },
|
||
a: { stroke: '#000', strokeWidth: 2, strokeDasharray: '4 4' }, // Пунктир
|
||
b: { stroke: '#D8000A', strokeWidth: 2 }, // Красная
|
||
c: { stroke: '#000', strokeWidth: 2, strokeDasharray: '1 3' }, // Точечная
|
||
d: { stroke: '#008000', strokeWidth: 2 }, // Зеленая
|
||
e: { stroke: '#1717D8', strokeWidth: 2 } // Синяя
|
||
};
|
||
|
||
function App() {
|
||
const [nodes, setNodes, onNodesChange] = useNodesState([])
|
||
const [edges, setEdges, onEdgesChange] = useEdgesState([])
|
||
const [flowInstance, setFlowInstance] = useState(null)
|
||
const [edgeType, setEdgeType] = useState('bool')
|
||
|
||
const onConnect = useCallback(
|
||
(params) => setEdges((edge) => {
|
||
const currentStyle = EDGE_STYLES[edgeType] || EDGE_STYLES['bool']
|
||
return addEdge({
|
||
...params,
|
||
type: 'smoothstep',
|
||
style: currentStyle,
|
||
data: {type: edgeType}
|
||
}, edge)
|
||
}), [setEdges, edgeType]
|
||
)
|
||
|
||
const onDragOver = useCallback((event) => {
|
||
event.preventDefault()
|
||
event.dataTransfer.dropEffect = 'move'
|
||
}, [])
|
||
|
||
const onDrop = useCallback(
|
||
(event) => {
|
||
event.preventDefault()
|
||
if (!flowInstance)
|
||
return
|
||
|
||
const dataStr = event.dataTransfer.getData('application/reactflow')
|
||
if (!dataStr)
|
||
return
|
||
|
||
const {type, label, inputCount, outputCount} = JSON.parse(dataStr)
|
||
const position = flowInstance.screenToFlowPosition({
|
||
x: event.clientX,
|
||
y: event.clientY
|
||
})
|
||
|
||
const newNode = {
|
||
id: `block_${Date.now()}`,
|
||
type: 'fbdBlock',
|
||
position,
|
||
data: {type, label, inputCount, outputCount}
|
||
}
|
||
setNodes((currentNodes) => [...currentNodes, newNode])
|
||
}, [flowInstance, setNodes]
|
||
)
|
||
|
||
function handleClear() {
|
||
if ((nodes.length > 0 || edges.length > 0) && window.confirm("Очистить холст?")) {
|
||
setNodes([])
|
||
setEdges([])
|
||
}
|
||
}
|
||
|
||
const handleExportSvg = useCallback(() => {
|
||
const reactFlowViewport = document.querySelector('.react-flow__viewport')
|
||
if (!reactFlowViewport || !nodes.length > 0) {
|
||
alert('Не удалось найти холст для экспорта')
|
||
return
|
||
}
|
||
|
||
const bounds = getNodesBounds(nodes)
|
||
const padding = 30
|
||
const widthExportWindow = bounds.width + padding * 2
|
||
const heightExportWindow = bounds.height + padding * 2
|
||
|
||
toSvg(reactFlowViewport, {
|
||
backgroundColor: 'white',
|
||
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'
|
||
}
|
||
}).then(async (dataUrl) => {
|
||
if ('showSaveFilePicker' in window) {
|
||
try {
|
||
const handle = await window.showSaveFilePicker({
|
||
suggestedName: 'fbd-scheme.svg',
|
||
types: [{
|
||
description: 'Векторная диаграмма SVG',
|
||
accept: {'image/svg+xml': ['.svg']}
|
||
}]
|
||
})
|
||
|
||
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.svg'
|
||
link.href = dataUrl
|
||
link.click()
|
||
}
|
||
}).catch((error) => {
|
||
console.error('Ошибка генерации SVG-файла: ', error)
|
||
alert('Не удалось сгененировать SVG-файл')
|
||
})
|
||
}, [nodes])
|
||
|
||
|
||
return(
|
||
<div className="App">
|
||
<ControlPanel
|
||
onClear = {handleClear}
|
||
onExportSvg = {handleExportSvg}
|
||
/>
|
||
<div className = "main-container">
|
||
<Toolbar
|
||
activeEdgeType = {edgeType}
|
||
onChangeEdgeType = {setEdgeType}
|
||
/>
|
||
<div className = "main-canvas-container">
|
||
<div className="canvas-container">
|
||
<ReactFlow
|
||
nodes = {nodes}
|
||
edges = {edges}
|
||
onNodesChange = {onNodesChange}
|
||
onEdgesChange = {onEdgesChange}
|
||
onConnect = {onConnect}
|
||
nodeTypes = {nodeTypes}
|
||
onInit = {setFlowInstance}
|
||
onDrop = {onDrop}
|
||
onDragOver = {onDragOver}
|
||
//fitView
|
||
>
|
||
<Controls/>
|
||
<Background variant = {BackgroundVariant.Lines} color = "#eee" gap = {10}/>
|
||
</ReactFlow>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default App |