diff --git a/web/src/components/CaseBuilder/NodesAndEdges.tsx b/web/src/components/CaseBuilder/NodesAndEdges.tsx index 1404897..b5713b0 100644 --- a/web/src/components/CaseBuilder/NodesAndEdges.tsx +++ b/web/src/components/CaseBuilder/NodesAndEdges.tsx @@ -8,6 +8,8 @@ import styles from './PipelineBlock.module.css'; export interface CustomNodeData { + [key:string]: unknown; + label: string; type: 'plant' | 'product' | 'center'; diff --git a/web/src/components/CaseBuilder/PipelineBlock.tsx b/web/src/components/CaseBuilder/PipelineBlock.tsx index f9d7683..348d85c 100644 --- a/web/src/components/CaseBuilder/PipelineBlock.tsx +++ b/web/src/components/CaseBuilder/PipelineBlock.tsx @@ -1,242 +1,233 @@ -import { CircularPlant, CircularProduct, CircularCenter } from "./CircularData"; -import { Node, Edge } from "@xyflow/react"; -import styles from "./PipelineBlock.module.css"; -import { ReactFlow, Background, Controls, MarkerType } from '@xyflow/react'; +import React, { useEffect, useCallback } from 'react'; +import dagre from 'dagre'; +import { + ReactFlow, + Background, + Controls, + Node, + Edge, + Connection, + MarkerType +} from '@xyflow/react'; +import { CircularPlant, CircularProduct, CircularCenter } from './CircularData'; +import CustomNode, { CustomNodeData } from './NodesAndEdges'; import Section from '../Common/Section'; import Card from '../Common/Card'; -import { useEffect, useCallback } from "react"; -import { Connection } from '@xyflow/react'; -import CustomNode, { CustomNodeData }from "./NodesAndEdges"; - +import styles from './PipelineBlock.module.css'; interface PipelineBlockProps { - onAddPlant: () => void; - onAddProduct: () => void; - onAddCenter: () => void; - onMovePlant: (name: string , x: number, y: number) => void; - onMoveProduct: (name: string, x: number, y: number) => void; - onMoveCenter: (name: string, x: number, y: number) => void; - onSetPlantInput: (plantName:string, productName: string) => void; - onAddPlantOutput: (plantName: string, productName: string) => void; - onAddCenterInput: (plantName: string, productName: string) => void; - onAddCenterOutput: (plantName: string, productName: string) => void; - onRemovePlant: (id:string) => void; - onRemoveProduct: (id: string) => void; - onRemoveCenter: (id: string) => void; - - products: Record; - plants: Record; - centers: Record; + onAddPlant: () => void; + onAddProduct: () => void; + onAddCenter: () => void; + onMovePlant: (name: string, x: number, y: number) => void; + onMoveProduct: (name: string, x: number, y: number) => void; + onMoveCenter: (name: string, x: number, y: number) => void; + onSetPlantInput: (plantName: string, productName: string) => void; + onAddPlantOutput: (plantName: string, productName: string) => void; + onAddCenterInput: (centerName: string, productName: string) => void; + onAddCenterOutput: (centerName: string, productName: string) => void; + onRemovePlant: (id: string) => void; + onRemoveProduct: (id: string) => void; + onRemoveCenter: (id: string) => void; + products: Record; + plants: Record; + centers: Record; } -const onNodeDoubleClick = () => {}; - -const handleNodesDelete = () => {}; -const handleEdgesDelete = () => {}; -const onLayout = () => {}; - -const PipelineBlock: React.FC = (props) => { - const nodes: Node[] = []; - const edges: Edge[] = []; - - let mapNameToType: Record = {}; - let hasNullPositions: boolean = false; - - const onConnect = (params: Connection) => { - const { source, target } = params; - if (!source || ! target) return; - - const sourceType = mapNameToType[source]; - const targetType = mapNameToType[target]; - - if (sourceType === "product" && targetType === "plant") { - props.onSetPlantInput(target,source); - } else if (sourceType === "plant" && targetType === "product") { - props.onAddPlantOutput(source, target); - } - else if (sourceType === "product" && targetType === "center") { - props.onAddCenterInput(target, source); - } - else if (sourceType === "center" && targetType === "product") { - props.onAddCenterOutput(source, target); - } - -}; - -const onNodeDragStop =(_:any, node: Node) => { - const { id, position, data} = node; - if (data.type === "plant") { - props.onMovePlant(id, position.x, position.y); - } - if (data.type === "product") { - props.onMoveProduct(id, position.x, position.y); - } - if (data.type === "center") { - props.onMoveCenter(id, position.x, position.y); - } - -}; - const handleNodesDelete = useCallback((deleted: Node[]) => { - deleted.forEach((n) => { - const type = mapNameToType[n.id]; - if (type === "plant") { - props.onRemovePlant(n.id); - } else if (type === "product") { - props.onRemoveProduct(n.id); - } else if (type === "center") { - props.onRemoveCenter!(n.id); +function getLayoutedNodesAndEdges( + nodes: Node[], + edges: Edge[] +): { nodes: Node[]; edges: Edge[] } { + const NODE_WIDTH = 125; + const NODE_HEIGHT = 45; + const g = new dagre.graphlib.Graph(); + g.setDefaultEdgeLabel(() => ({})); + g.setGraph({ rankdir: 'LR' }); + nodes.forEach(n => g.setNode(n.id, { width: NODE_WIDTH, height: NODE_HEIGHT })); + edges.forEach(e => g.setEdge(e.source, e.target)); + dagre.layout(g); + const layouted = nodes.map(n => { + const d = g.node(n.id)!; + return { + ...n, + position: { + x: d.x - NODE_WIDTH / 2, + y: d.y - NODE_HEIGHT / 2 } - }); - }, [props, mapNameToType]); - - for (const [productName, product] of Object.entries(props.products) as [string, CircularProduct][]) { - if(!product.x || !product.y) hasNullPositions = true; - mapNameToType[productName] = "product"; - nodes.push({ - id: product.uid, - type: "default", - data: {label: product.name, type: 'product'}, - position: { x:product.x, y:product.y}, - className: 'ProductNode' - }); - } - for (const [plantName, plant] of Object.entries(props.plants) as [string, CircularPlant][]) { - if(!plant.x || !plant.y) hasNullPositions = true; - mapNameToType[plantName] = "plant"; - nodes.push({ - id: plant.uid, - type: "default", - data: {label: plant.name, type: 'plant'}, - position: { x:plant.x, y:plant.y}, - className: 'PlantNode' - }); + }; + }); + return { nodes: layouted, edges }; +} - if (plant) { - for (const inputProduct of plant.inputs){ - edges.push({ - id: `${inputProduct}-${plantName}`, - source: inputProduct, - target: plantName, - animated: true, - style: { stroke: "black" }, - markerEnd: { - type: MarkerType.ArrowClosed, - }, - }); - } - for (const outputProduct of plant.outputs ?? []) { - edges.push({ - id: `${plantName}-${outputProduct}`, - source: plantName, - target: outputProduct, - animated: true, - style: { stroke: 'black' }, - markerEnd: { type: MarkerType.ArrowClosed }, +const PipelineBlock: React.FC = props => { + const nodes: Node[] = []; + const edges: Edge[] = []; + const mapNameToType: Record = {}; + let hasNullPositions = false; + Object.entries(props.products).forEach(([key, product]) => { + if (!product.x || !product.y) hasNullPositions = true; + mapNameToType[key] = 'product'; + nodes.push({ + id: product.uid, + type: 'default', + data: { label: product.name, type: 'product' }, + position: { x: product.x, y: product.y }, + className: 'ProductNode' }); - } - - } - - } - for (const [centerName, center] of Object.entries(props.centers)) { - mapNameToType[centerName] = "center"; + }); + Object.entries(props.plants).forEach(([key, plant]) => { + if (!plant.x || !plant.y) hasNullPositions = true; + mapNameToType[key] = 'plant'; + nodes.push({ + id: plant.uid, + type: 'default', + data: { label: plant.name, type: 'plant' }, + position: { x: plant.x, y: plant.y }, + className: 'PlantNode' + }); + plant.inputs.forEach(input => { + edges.push({ + id: `${input}-${key}-in`, + source: input, + target: key, + animated: true, + style: { stroke: 'black' }, + markerEnd: { type: MarkerType.ArrowClosed } + }); + }); + plant.outputs.forEach(output => { + edges.push({ + id: `${key}-${output}-out`, + source: key, + target: output, + animated: true, + style: { stroke: 'black' }, + markerEnd: { type: MarkerType.ArrowClosed } + }); + }); + }); + Object.entries(props.centers).forEach(([key, center]) => { + if (!center.x || !center.y) hasNullPositions = true; + mapNameToType[key] = 'center'; nodes.push({ id: center.uid, - type: "default", - data: { label: center.name, type: "center"}, - position: {x: center.x, y: center.y}, + type: 'default', + data: { label: center.name, type: 'center' }, + position: { x: center.x, y: center.y }, className: 'CenterNode' }); if (center.input) { - edges.push({ - id: `${center.input}-${centerName}`, + edges.push({ + id: `${center.input}-${key}-in`, source: center.input, - target:centerName, + target: key, animated: true, - style: { stroke: "black"}, - markerEnd: { type: MarkerType.ArrowClosed}, + style: { stroke: 'black' }, + markerEnd: { type: MarkerType.ArrowClosed } }); } - for (const out of center.output) { + center.output.forEach(out => { edges.push({ - id: `${centerName}-${out}`, - source: centerName, - target:out, + id: `${key}-${out}-out`, + source: key, + target: out, animated: true, - style: { stroke: "black"}, - markerEnd: { type: MarkerType.ArrowClosed}, + style: { stroke: 'black' }, + markerEnd: { type: MarkerType.ArrowClosed } }); - } - } - - - - useEffect(() => { - if (hasNullPositions) onLayout(); - - }, [hasNullPositions]); - - -return ( -<> -
- -
- { + const { source, target } = params; + if (!source || !target) return; + const sourceType = mapNameToType[source]; + const targetType = mapNameToType[target]; + if (sourceType === 'product' && targetType === 'plant') props.onSetPlantInput(target, source); + else if (sourceType === 'plant' && targetType === 'product') props.onAddPlantOutput(source, target); + else if (sourceType === 'product' && targetType === 'center') props.onAddCenterInput(target, source); + else if (sourceType === 'center' && targetType === 'product') props.onAddCenterOutput(source, target); + }; + + const onNodeDragStop = (_: any, node: Node) => { + const { id, position, data } = node; + if (data.type === 'plant') props.onMovePlant(id, position.x, position.y); + if (data.type === 'product') props.onMoveProduct(id, position.x, position.y); + if (data.type === 'center') props.onMoveCenter(id, position.x, position.y); + }; + + const handleNodesDelete = useCallback( + (deleted: Node[]) => { + deleted.forEach(n => { + const type = mapNameToType[n.id]; + if (type === 'plant') props.onRemovePlant(n.id); + else if (type === 'product') props.onRemoveProduct(n.id); + else if (type === 'center') props.onRemoveCenter(n.id); + }); + }, + [props] + ); + + const onLayout = () => { + const { nodes: ln, edges: le } = getLayoutedNodesAndEdges(nodes, edges); + ln.forEach(n => { + const { id, position, data } = n; + if (data.type === 'plant') props.onMovePlant(id, position.x, position.y); + else if (data.type === 'product') props.onMoveProduct(id, position.x, position.y); + else props.onMoveCenter(id, position.x, position.y); + }); + }; - nodes={nodes} - edges={edges} - onNodeDoubleClick={onNodeDoubleClick} - onNodeDragStop={onNodeDragStop} - onConnect={onConnect} - onNodesDelete={handleNodesDelete} - deleteKeyCode="Delete" - maxZoom={1.25} - minZoom={0.5} - snapToGrid={true} - preventScrolling={false} - nodeTypes={{ default: CustomNode }} -> - - - -
-
- - - - - -
-
- -); + useEffect(() => { + if (hasNullPositions) onLayout(); + }, [hasNullPositions]); + + return ( + <> +
+ +
+ + + + +
+
+ + + + + +
+
+ + ); }; -export default PipelineBlock; \ No newline at end of file +export default PipelineBlock; + \ No newline at end of file