2024-06-27 14:43:06 +03:00
|
|
|
'use client';
|
|
|
|
|
2024-07-24 18:11:28 +03:00
|
|
|
import { toSvg } from 'html-to-image';
|
2024-07-23 23:03:58 +03:00
|
|
|
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
2024-07-24 18:11:28 +03:00
|
|
|
import { toast } from 'react-toastify';
|
2024-07-20 18:26:32 +03:00
|
|
|
import {
|
2024-07-24 18:11:28 +03:00
|
|
|
Background,
|
|
|
|
getNodesBounds,
|
|
|
|
getViewportForBounds,
|
2024-07-23 23:03:58 +03:00
|
|
|
Node,
|
2024-07-20 18:26:32 +03:00
|
|
|
NodeChange,
|
|
|
|
NodeTypes,
|
|
|
|
ProOptions,
|
|
|
|
ReactFlow,
|
|
|
|
useEdgesState,
|
|
|
|
useNodesState,
|
2024-07-23 23:03:58 +03:00
|
|
|
useOnSelectionChange,
|
|
|
|
useReactFlow
|
2024-07-20 18:26:32 +03:00
|
|
|
} from 'reactflow';
|
2024-06-27 14:43:06 +03:00
|
|
|
|
2024-07-20 18:26:32 +03:00
|
|
|
import Overlay from '@/components/ui/Overlay';
|
|
|
|
import AnimateFade from '@/components/wrap/AnimateFade';
|
2024-06-27 14:43:06 +03:00
|
|
|
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
|
|
|
import { useOSS } from '@/context/OssContext';
|
2024-07-23 23:03:58 +03:00
|
|
|
import { PARAMETER } from '@/utils/constants';
|
2024-07-24 18:11:28 +03:00
|
|
|
import { errors } from '@/utils/labels';
|
2024-06-27 14:43:06 +03:00
|
|
|
|
2024-07-20 18:26:32 +03:00
|
|
|
import { useOssEdit } from '../OssEditContext';
|
2024-06-27 14:43:06 +03:00
|
|
|
import InputNode from './InputNode';
|
|
|
|
import OperationNode from './OperationNode';
|
2024-07-20 18:26:32 +03:00
|
|
|
import ToolbarOssGraph from './ToolbarOssGraph';
|
2024-06-27 14:43:06 +03:00
|
|
|
|
2024-07-23 23:03:58 +03:00
|
|
|
interface OssFlowProps {
|
|
|
|
isModified: boolean;
|
|
|
|
setIsModified: React.Dispatch<React.SetStateAction<boolean>>;
|
2024-07-24 18:11:28 +03:00
|
|
|
showGrid: boolean;
|
|
|
|
setShowGrid: React.Dispatch<React.SetStateAction<boolean>>;
|
2024-07-23 23:03:58 +03:00
|
|
|
}
|
|
|
|
|
2024-07-24 18:11:28 +03:00
|
|
|
function OssFlow({ isModified, setIsModified, showGrid, setShowGrid }: OssFlowProps) {
|
|
|
|
const { calculateHeight, colors } = useConceptOptions();
|
2024-06-27 14:43:06 +03:00
|
|
|
const model = useOSS();
|
2024-07-20 18:26:32 +03:00
|
|
|
const controller = useOssEdit();
|
2024-07-23 23:03:58 +03:00
|
|
|
const flow = useReactFlow();
|
2024-06-27 14:43:06 +03:00
|
|
|
|
2024-07-21 22:50:43 +03:00
|
|
|
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
|
|
|
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
2024-07-23 23:03:58 +03:00
|
|
|
const [toggleReset, setToggleReset] = useState(false);
|
|
|
|
|
|
|
|
const onSelectionChange = useCallback(
|
|
|
|
({ nodes }: { nodes: Node[] }) => {
|
|
|
|
controller.setSelected(nodes.map(node => Number(node.id)));
|
|
|
|
console.log(nodes);
|
|
|
|
},
|
|
|
|
[controller]
|
|
|
|
);
|
|
|
|
|
|
|
|
useOnSelectionChange({
|
|
|
|
onChange: onSelectionChange
|
|
|
|
});
|
2024-07-21 22:50:43 +03:00
|
|
|
|
|
|
|
useLayoutEffect(() => {
|
|
|
|
if (!model.schema) {
|
|
|
|
setNodes([]);
|
|
|
|
setEdges([]);
|
2024-07-23 23:03:58 +03:00
|
|
|
} else {
|
|
|
|
setNodes(
|
|
|
|
model.schema.items.map(operation => ({
|
|
|
|
id: String(operation.id),
|
|
|
|
data: { label: operation.alias },
|
|
|
|
position: { x: operation.position_x, y: operation.position_y },
|
|
|
|
type: operation.operation_type.toString()
|
|
|
|
}))
|
|
|
|
);
|
|
|
|
setEdges(
|
|
|
|
model.schema.arguments.map((argument, index) => ({
|
|
|
|
id: String(index),
|
|
|
|
source: String(argument.argument),
|
|
|
|
target: String(argument.operation),
|
|
|
|
targetHandle:
|
|
|
|
model.schema!.operationByID.get(argument.argument)!.position_x >
|
|
|
|
model.schema!.operationByID.get(argument.operation)!.position_x
|
|
|
|
? 'right'
|
|
|
|
: 'left'
|
|
|
|
}))
|
|
|
|
);
|
2024-07-21 22:50:43 +03:00
|
|
|
}
|
2024-07-23 23:03:58 +03:00
|
|
|
setTimeout(() => {
|
|
|
|
setIsModified(false);
|
|
|
|
}, PARAMETER.graphRefreshDelay);
|
|
|
|
}, [model.schema, setNodes, setEdges, setIsModified, toggleReset]);
|
2024-06-27 14:43:06 +03:00
|
|
|
|
2024-07-21 15:17:36 +03:00
|
|
|
const getPositions = useCallback(
|
|
|
|
() =>
|
|
|
|
nodes.map(node => ({
|
|
|
|
id: Number(node.id),
|
|
|
|
position_x: node.position.x,
|
|
|
|
position_y: node.position.y
|
|
|
|
})),
|
|
|
|
[nodes]
|
|
|
|
);
|
|
|
|
|
2024-07-20 18:26:32 +03:00
|
|
|
const handleNodesChange = useCallback(
|
|
|
|
(changes: NodeChange[]) => {
|
2024-07-23 23:03:58 +03:00
|
|
|
if (changes.some(change => change.type === 'position' && change.position)) {
|
|
|
|
setIsModified(true);
|
|
|
|
}
|
2024-07-20 18:26:32 +03:00
|
|
|
onNodesChange(changes);
|
|
|
|
},
|
2024-07-23 23:03:58 +03:00
|
|
|
[onNodesChange, setIsModified]
|
2024-07-20 18:26:32 +03:00
|
|
|
);
|
|
|
|
|
2024-07-23 23:03:58 +03:00
|
|
|
const handleSavePositions = useCallback(() => {
|
|
|
|
controller.savePositions(getPositions(), () => setIsModified(false));
|
|
|
|
}, [controller, getPositions, setIsModified]);
|
2024-07-20 18:26:32 +03:00
|
|
|
|
|
|
|
const handleCreateOperation = useCallback(() => {
|
2024-07-23 23:03:58 +03:00
|
|
|
const center = flow.project({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
|
|
|
|
console.log(center);
|
|
|
|
controller.promptCreateOperation(center.x, center.y, getPositions());
|
|
|
|
}, [controller, getPositions, flow]);
|
|
|
|
|
|
|
|
const handleDeleteOperation = useCallback(() => {
|
|
|
|
if (controller.selected.length !== 1) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
controller.deleteOperation(controller.selected[0], getPositions());
|
|
|
|
}, [controller, getPositions]);
|
|
|
|
|
2024-07-24 18:11:28 +03:00
|
|
|
const handleFitView = useCallback(() => {
|
|
|
|
flow.fitView({ duration: PARAMETER.zoomDuration });
|
|
|
|
}, [flow]);
|
|
|
|
|
|
|
|
const handleResetPositions = useCallback(() => {
|
|
|
|
setToggleReset(prev => !prev);
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
const handleSaveImage = useCallback(() => {
|
|
|
|
const canvas: HTMLElement | null = document.querySelector('.react-flow__viewport');
|
|
|
|
if (canvas === null) {
|
|
|
|
toast.error(errors.imageFailed);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const imageWidth = PARAMETER.ossImageWidth;
|
|
|
|
const imageHeight = PARAMETER.ossImageHeight;
|
|
|
|
const nodesBounds = getNodesBounds(nodes);
|
|
|
|
const viewport = getViewportForBounds(nodesBounds, imageWidth, imageHeight, 0.5, 2);
|
|
|
|
toSvg(canvas, {
|
|
|
|
backgroundColor: colors.bgDefault,
|
|
|
|
width: imageWidth,
|
|
|
|
height: imageHeight,
|
|
|
|
style: {
|
|
|
|
width: String(imageWidth),
|
|
|
|
height: String(imageHeight),
|
|
|
|
transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.then(dataURL => {
|
|
|
|
const a = document.createElement('a');
|
|
|
|
a.setAttribute('download', 'reactflow.svg');
|
|
|
|
a.setAttribute('href', dataURL);
|
|
|
|
a.click();
|
|
|
|
})
|
|
|
|
.catch(error => {
|
|
|
|
console.error(error);
|
|
|
|
toast.error(errors.imageFailed);
|
|
|
|
});
|
|
|
|
}, [colors, nodes]);
|
|
|
|
|
2024-07-23 23:03:58 +03:00
|
|
|
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
|
|
|
// Hotkeys implementation
|
|
|
|
if (controller.isProcessing) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!controller.isMutable) {
|
|
|
|
return;
|
|
|
|
}
|
2024-07-24 18:11:28 +03:00
|
|
|
if ((event.ctrlKey || event.metaKey) && event.key === 's') {
|
|
|
|
event.preventDefault();
|
|
|
|
event.stopPropagation();
|
|
|
|
handleSavePositions();
|
|
|
|
return;
|
|
|
|
}
|
2024-07-23 23:03:58 +03:00
|
|
|
if (event.key === 'Delete') {
|
|
|
|
event.preventDefault();
|
|
|
|
event.stopPropagation();
|
|
|
|
handleDeleteOperation();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
2024-06-27 14:43:06 +03:00
|
|
|
|
2024-07-21 15:17:36 +03:00
|
|
|
const proOptions: ProOptions = useMemo(() => ({ hideAttribution: true }), []);
|
2024-07-20 18:26:32 +03:00
|
|
|
const canvasWidth = useMemo(() => 'calc(100vw - 1rem)', []);
|
2024-06-27 14:43:06 +03:00
|
|
|
const canvasHeight = useMemo(() => calculateHeight('1.75rem + 4px'), [calculateHeight]);
|
|
|
|
|
2024-07-20 18:26:32 +03:00
|
|
|
const OssNodeTypes: NodeTypes = useMemo(
|
|
|
|
() => ({
|
|
|
|
synthesis: OperationNode,
|
|
|
|
input: InputNode
|
|
|
|
}),
|
|
|
|
[]
|
|
|
|
);
|
|
|
|
|
2024-07-21 15:17:36 +03:00
|
|
|
const graph = useMemo(
|
|
|
|
() => (
|
|
|
|
<ReactFlow
|
|
|
|
nodes={nodes}
|
|
|
|
edges={edges}
|
|
|
|
onNodesChange={handleNodesChange}
|
2024-07-23 23:03:58 +03:00
|
|
|
onEdgesChange={onEdgesChange}
|
2024-07-21 15:17:36 +03:00
|
|
|
fitView
|
|
|
|
proOptions={proOptions}
|
|
|
|
nodeTypes={OssNodeTypes}
|
2024-07-21 22:50:43 +03:00
|
|
|
maxZoom={2}
|
|
|
|
minZoom={0.75}
|
2024-07-23 23:03:58 +03:00
|
|
|
nodesConnectable={false}
|
2024-07-24 18:11:28 +03:00
|
|
|
snapToGrid={true}
|
|
|
|
snapGrid={[10, 10]}
|
|
|
|
>
|
|
|
|
{showGrid ? <Background gap={10} /> : null}
|
|
|
|
</ReactFlow>
|
2024-07-21 15:17:36 +03:00
|
|
|
),
|
2024-07-24 18:11:28 +03:00
|
|
|
[nodes, edges, proOptions, handleNodesChange, onEdgesChange, OssNodeTypes, showGrid]
|
2024-07-21 15:17:36 +03:00
|
|
|
);
|
|
|
|
|
2024-06-27 14:43:06 +03:00
|
|
|
return (
|
2024-07-23 23:03:58 +03:00
|
|
|
<AnimateFade tabIndex={-1} onKeyDown={handleKeyDown}>
|
2024-07-20 18:26:32 +03:00
|
|
|
<Overlay position='top-0 pt-1 right-1/2 translate-x-1/2' className='rounded-b-2xl cc-blur'>
|
2024-07-23 23:03:58 +03:00
|
|
|
<ToolbarOssGraph
|
|
|
|
isModified={isModified}
|
2024-07-24 18:11:28 +03:00
|
|
|
showGrid={showGrid}
|
|
|
|
onFitView={handleFitView}
|
2024-07-23 23:03:58 +03:00
|
|
|
onCreate={handleCreateOperation}
|
|
|
|
onDelete={handleDeleteOperation}
|
2024-07-24 18:11:28 +03:00
|
|
|
onResetPositions={handleResetPositions}
|
2024-07-23 23:03:58 +03:00
|
|
|
onSavePositions={handleSavePositions}
|
2024-07-24 18:11:28 +03:00
|
|
|
onSaveImage={handleSaveImage}
|
|
|
|
toggleShowGrid={() => setShowGrid(prev => !prev)}
|
2024-07-23 23:03:58 +03:00
|
|
|
/>
|
2024-07-20 18:26:32 +03:00
|
|
|
</Overlay>
|
|
|
|
<div className='relative' style={{ height: canvasHeight, width: canvasWidth }}>
|
2024-07-21 15:17:36 +03:00
|
|
|
{graph}
|
2024-07-20 18:26:32 +03:00
|
|
|
</div>
|
|
|
|
</AnimateFade>
|
2024-06-27 14:43:06 +03:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export default OssFlow;
|