diff --git a/Data/Server/WebUI/src/Flow_Editor.jsx b/Data/Server/WebUI/src/Flow_Editor.jsx
index 4624eec..cd6b71e 100644
--- a/Data/Server/WebUI/src/Flow_Editor.jsx
+++ b/Data/Server/WebUI/src/Flow_Editor.jsx
@@ -23,7 +23,7 @@ import { SketchPicker } from "react-color";
import "reactflow/dist/style.css";
export default function FlowEditor({
- flowId, //Used to Fix Grid Issues Across Multiple Flow Tabs
+ flowId,
nodes,
edges,
setNodes,
@@ -33,6 +33,7 @@ export default function FlowEditor({
}) {
const wrapperRef = useRef(null);
const { project } = useReactFlow();
+
const [contextMenu, setContextMenu] = useState(null);
const [edgeContextMenu, setEdgeContextMenu] = useState(null);
const [selectedEdgeId, setSelectedEdgeId] = useState(null);
@@ -44,6 +45,15 @@ export default function FlowEditor({
const [tempColor, setTempColor] = useState({ hex: "#58a6ff" });
const [pickerPos, setPickerPos] = useState({ x: 0, y: 0 });
+ // helper-line state
+ // guides: array of { xFlow, xPx } or { yFlow, yPx } for stationary nodes
+ const [guides, setGuides] = useState([]);
+ // activeGuides: array of { xPx } or { yPx } to draw
+ const [activeGuides, setActiveGuides] = useState([]);
+
+ // store moving node flow-size on drag start
+ const movingFlowSize = useRef({ width: 0, height: 0 });
+
const edgeStyles = {
step: "step",
curved: "bezier",
@@ -52,64 +62,167 @@ export default function FlowEditor({
const animationStyles = {
dashes: { animated: true, style: { strokeDasharray: "6 3" } },
- dots: { animated: true, style: { strokeDasharray: "2 4" } },
- none: { animated: false, style: {} }
+ dots: { animated: true, style: { strokeDasharray: "2 4" } },
+ none: { animated: false, style: {} }
};
- const onDrop = useCallback(
- (event) => {
- event.preventDefault();
- const type = event.dataTransfer.getData("application/reactflow");
- if (!type) return;
- const bounds = wrapperRef.current.getBoundingClientRect();
- const position = project({ x: event.clientX - bounds.left, y: event.clientY - bounds.top });
- const id = "node-" + Date.now();
- const nodeMeta = Object.values(categorizedNodes).flat().find((n) => n.type === type);
- const newNode = {
- id,
- type,
- position,
- data: {
- label: nodeMeta?.label || type,
- content: nodeMeta?.content
- },
- dragHandle: '.borealis-node-header' // <-- Add this line
- };
- setNodes((nds) => [...nds, newNode]);
- },
- [project, setNodes, categorizedNodes]
- );
+ // Compute edge-only guides and capture moving node flow-size
+ const computeGuides = useCallback((dragNode) => {
+ if (!wrapperRef.current) return;
+ const parentRect = wrapperRef.current.getBoundingClientRect();
+
+ // measure moving node in pixel space
+ const dragEl = wrapperRef.current.querySelector(
+ `.react-flow__node[data-id="${dragNode.id}"]`
+ );
+ if (dragEl) {
+ const dr = dragEl.getBoundingClientRect();
+ const relLeft = dr.left - parentRect.left;
+ const relTop = dr.top - parentRect.top;
+ const relRight = relLeft + dr.width;
+ const relBottom = relTop + dr.height;
+
+ // project pixel corners to flow coords
+ const pTL = project({ x: relLeft, y: relTop });
+ const pTR = project({ x: relRight, y: relTop });
+ const pBL = project({ x: relLeft, y: relBottom });
+
+ movingFlowSize.current = {
+ width: pTR.x - pTL.x,
+ height: pBL.y - pTL.y
+ };
+ }
+
+ const lines = [];
+ nodes.forEach((n) => {
+ if (n.id === dragNode.id) return;
+ const el = wrapperRef.current.querySelector(
+ `.react-flow__node[data-id="${n.id}"]`
+ );
+ if (!el) return;
+ const r = el.getBoundingClientRect();
+ const relLeft = r.left - parentRect.left;
+ const relTop = r.top - parentRect.top;
+ const relRight = relLeft + r.width;
+ const relBottom = relTop + r.height;
+
+ // project pixel to flow coords
+ const pTL = project({ x: relLeft, y: relTop });
+ const pTR = project({ x: relRight, y: relTop });
+ const pBL = project({ x: relLeft, y: relBottom });
+
+ // vertical guides: left edge, right edge
+ lines.push({ xFlow: pTL.x, xPx: relLeft });
+ lines.push({ xFlow: pTR.x, xPx: relRight });
+
+ // horizontal guides: top edge, bottom edge
+ lines.push({ yFlow: pTL.y, yPx: relTop });
+ lines.push({ yFlow: pBL.y, yPx: relBottom });
+ });
+ setGuides(lines);
+ }, [nodes, project]);
+
+ // Snap & show only matching guides within threshold during drag
+ const onNodeDrag = useCallback((_, node) => {
+ const threshold = 5;
+ let snapX = null, snapY = null;
+ const show = [];
+ const { width: fw, height: fh } = movingFlowSize.current;
+
+ guides.forEach((ln) => {
+ if (ln.xFlow != null) {
+ // moving left edge to stationary edges
+ if (Math.abs(node.position.x - ln.xFlow) < threshold) {
+ snapX = ln.xFlow;
+ show.push({ xPx: ln.xPx });
+ }
+ // moving right edge to stationary edges
+ else if (Math.abs(node.position.x + fw - ln.xFlow) < threshold) {
+ snapX = ln.xFlow - fw;
+ show.push({ xPx: ln.xPx });
+ }
+ }
+ if (ln.yFlow != null) {
+ // moving top edge
+ if (Math.abs(node.position.y - ln.yFlow) < threshold) {
+ snapY = ln.yFlow;
+ show.push({ yPx: ln.yPx });
+ }
+ // moving bottom edge
+ else if (Math.abs(node.position.y + fh - ln.yFlow) < threshold) {
+ snapY = ln.yFlow - fh;
+ show.push({ yPx: ln.yPx });
+ }
+ }
+ });
+
+ if (snapX !== null || snapY !== null) {
+ setNodes((nds) =>
+ applyNodeChanges(
+ [{
+ id: node.id,
+ type: "position",
+ position: {
+ x: snapX !== null ? snapX : node.position.x,
+ y: snapY !== null ? snapY : node.position.y
+ }
+ }],
+ nds
+ )
+ );
+ setActiveGuides(show);
+ } else {
+ setActiveGuides([]);
+ }
+ }, [guides, setNodes]);
+
+ const onDrop = useCallback((event) => {
+ event.preventDefault();
+ const type = event.dataTransfer.getData("application/reactflow");
+ if (!type) return;
+ const bounds = wrapperRef.current.getBoundingClientRect();
+ const position = project({
+ x: event.clientX - bounds.left,
+ y: event.clientY - bounds.top
+ });
+ const id = "node-" + Date.now();
+ const nodeMeta = Object.values(categorizedNodes).flat().find((n) => n.type === type);
+ const newNode = {
+ id,
+ type,
+ position,
+ data: {
+ label: nodeMeta?.label || type,
+ content: nodeMeta?.content
+ },
+ dragHandle: ".borealis-node-header"
+ };
+ setNodes((nds) => [...nds, newNode]);
+ }, [project, setNodes, categorizedNodes]);
const onDragOver = useCallback((event) => {
event.preventDefault();
event.dataTransfer.dropEffect = "move";
}, []);
- const onConnect = useCallback(
- (params) => {
- setEdges((eds) =>
- addEdge(
- {
- ...params,
- type: "bezier",
- animated: true,
- style: { strokeDasharray: "6 3", stroke: "#58a6ff" }
- },
- eds
- )
- );
- },
- [setEdges]
- );
+ const onConnect = useCallback((params) => {
+ setEdges((eds) =>
+ addEdge({
+ ...params,
+ type: "bezier",
+ animated: true,
+ style: { strokeDasharray: "6 3", stroke: "#58a6ff" }
+ }, eds)
+ );
+ }, [setEdges]);
- const onNodesChange = useCallback(
- (changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
- [setNodes]
- );
- const onEdgesChange = useCallback(
- (changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
- [setEdges]
- );
+ const onNodesChange = useCallback((changes) => {
+ setNodes((nds) => applyNodeChanges(changes, nds));
+ }, [setNodes]);
+
+ const onEdgesChange = useCallback((changes) => {
+ setEdges((eds) => applyEdgeChanges(changes, eds));
+ }, [setEdges]);
const handleRightClick = (e, node) => {
e.preventDefault();
@@ -123,76 +236,64 @@ export default function FlowEditor({
};
const changeEdgeType = (newType) => {
- setEdges((eds) =>
- eds.map((e) =>
- e.id === selectedEdgeId ? { ...e, type: edgeStyles[newType] } : e
- )
- );
+ setEdges((eds) => eds.map((e) =>
+ e.id === selectedEdgeId ? { ...e, type: edgeStyles[newType] } : e
+ ));
setEdgeContextMenu(null);
};
const changeEdgeAnimation = (newAnim) => {
- setEdges((eds) =>
- eds.map((e) => {
- if (e.id !== selectedEdgeId) return e;
- const strokeColor = e.style?.stroke || "#58a6ff";
- const anim = animationStyles[newAnim] || {};
- return {
- ...e,
- animated: anim.animated,
- style: { ...anim.style, stroke: strokeColor },
- markerEnd: e.markerEnd ? { ...e.markerEnd, color: strokeColor } : undefined
- };
- })
- );
+ setEdges((eds) => eds.map((e) => {
+ if (e.id !== selectedEdgeId) return e;
+ const strokeColor = e.style?.stroke || "#58a6ff";
+ const anim = animationStyles[newAnim] || {};
+ return {
+ ...e,
+ animated: anim.animated,
+ style: { ...anim.style, stroke: strokeColor },
+ markerEnd: e.markerEnd ? { ...e.markerEnd, color: strokeColor } : undefined
+ };
+ }));
setEdgeContextMenu(null);
};
const handleColorChange = (color) => {
- setEdges((eds) =>
- eds.map((e) => {
- if (e.id !== selectedEdgeId) return e;
- const updated = { ...e };
- if (colorPickerMode === "stroke") {
- updated.style = { ...e.style, stroke: color.hex };
- if (e.markerEnd) updated.markerEnd = { ...e.markerEnd, color: color.hex };
- } else if (colorPickerMode === "labelText") {
- updated.labelStyle = { ...e.labelStyle, fill: color.hex };
- } else if (colorPickerMode === "labelBg") {
- updated.labelBgStyle = { ...e.labelBgStyle, fill: color.hex, fillOpacity: labelOpacity };
- }
- return updated;
- })
- );
+ setEdges((eds) => eds.map((e) => {
+ if (e.id !== selectedEdgeId) return e;
+ const updated = { ...e };
+ if (colorPickerMode === "stroke") {
+ updated.style = { ...e.style, stroke: color.hex };
+ if (e.markerEnd) updated.markerEnd = { ...e.markerEnd, color: color.hex };
+ } else if (colorPickerMode === "labelText") {
+ updated.labelStyle = { ...e.labelStyle, fill: color.hex };
+ } else if (colorPickerMode === "labelBg") {
+ updated.labelBgStyle = { ...e.labelBgStyle, fill: color.hex, fillOpacity: labelOpacity };
+ }
+ return updated;
+ }));
};
const handleAddLabel = () => {
- setEdges((eds) =>
- eds.map((e) =>
- e.id === selectedEdgeId ? { ...e, label: "New Label" } : e
- )
- );
+ setEdges((eds) => eds.map((e) =>
+ e.id === selectedEdgeId ? { ...e, label: "New Label" } : e
+ ));
setEdgeContextMenu(null);
};
const handleEditLabel = () => {
const newText = prompt("Enter label text:");
if (newText !== null) {
- setEdges((eds) =>
- eds.map((e) =>
- e.id === selectedEdgeId ? { ...e, label: newText } : e
- )
- );
+ setEdges((eds) => eds.map((e) =>
+ e.id === selectedEdgeId ? { ...e, label: newText } : e
+ ));
}
setEdgeContextMenu(null);
};
const handleRemoveLabel = () => {
- setEdges((eds) =>
- eds.map((e) =>
- e.id === selectedEdgeId ? { ...e, label: undefined } : e
- )
- );
+ setEdges((eds) => eds.map((e) =>
+ e.id === selectedEdgeId ? { ...e, label: undefined } : e
+ ));
setEdgeContextMenu(null);
};
@@ -204,22 +305,20 @@ export default function FlowEditor({
};
const applyLabelStyleExtras = () => {
- setEdges((eds) =>
- eds.map((e) =>
- e.id === selectedEdgeId
- ? {
- ...e,
- labelBgPadding: labelPadding,
- labelBgStyle: {
- ...e.labelBgStyle,
- fillOpacity: labelOpacity,
- rx: labelBorderRadius,
- ry: labelBorderRadius
- }
+ setEdges((eds) => eds.map((e) =>
+ e.id === selectedEdgeId
+ ? {
+ ...e,
+ labelBgPadding: labelPadding,
+ labelBgStyle: {
+ ...e.labelBgStyle,
+ fillOpacity: labelOpacity,
+ rx: labelBorderRadius,
+ ry: labelBorderRadius
}
- : e
- )
- );
+ }
+ : e
+ ));
setEdgeContextMenu(null);
};
@@ -242,66 +341,60 @@ export default function FlowEditor({
onNodeContextMenu={handleRightClick}
onEdgeContextMenu={handleEdgeRightClick}
defaultViewport={{ x: 0, y: 0, zoom: 1.5 }}
- edgeOptions={{
- type: "bezier",
- animated: true,
- style: { strokeDasharray: "6 3", stroke: "#58a6ff" }
- }}
+ edgeOptions={{ type: "bezier", animated: true, style: { strokeDasharray: "6 3", stroke: "#58a6ff" } }}
proOptions={{ hideAttribution: true }}
+ onNodeDragStart={(_, node) => computeGuides(node)}
+ onNodeDrag={onNodeDrag}
+ onNodeDragStop={() => { setGuides([]); setActiveGuides([]); }}
>