Skip to content

Commit

Permalink
Improve block positioning logic to handle collisions and dynamic dime…
Browse files Browse the repository at this point in the history
…nsions
  • Loading branch information
Abhi1992002 committed Oct 23, 2024
1 parent e5ea62d commit 9a1d0ca
Show file tree
Hide file tree
Showing 2 changed files with 172 additions and 10 deletions.
106 changes: 96 additions & 10 deletions autogpt_platform/frontend/src/components/Flow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,12 @@ import {
import "@xyflow/react/dist/style.css";
import { CustomNode } from "./CustomNode";
import "./flow.css";
import { Link } from "@/lib/autogpt-server-api";
import { getTypeColor, filterBlocksByType } from "@/lib/utils";
import { BlockUIType, Link } from "@/lib/autogpt-server-api";
import {
getTypeColor,
filterBlocksByType,
findNewlyAddedBlockCoordinates,
} from "@/lib/utils";
import { history } from "./history";
import { CustomEdge } from "./CustomEdge";
import ConnectionLine from "./ConnectionLine";
Expand Down Expand Up @@ -57,15 +61,30 @@ type FlowContextType = {
getNextNodeId: () => string;
};

export type NodeDimension = {
[nodeId: string]: {
x: number;
y: number;
width: number;
height: number;
};
};

export const FlowContext = createContext<FlowContextType | null>(null);

const FlowEditor: React.FC<{
flowID?: string;
template?: boolean;
className?: string;
}> = ({ flowID, template, className }) => {
const { addNodes, addEdges, getNode, deleteElements, updateNode } =
useReactFlow<CustomNode, CustomEdge>();
const {
addNodes,
addEdges,
getNode,
deleteElements,
updateNode,
setViewport,
} = useReactFlow<CustomNode, CustomEdge>();
const [nodeId, setNodeId] = useState<number>(1);
const [copiedNodes, setCopiedNodes] = useState<CustomNode[]>([]);
const [copiedEdges, setCopiedEdges] = useState<CustomEdge[]>([]);
Expand Down Expand Up @@ -110,6 +129,9 @@ const FlowEditor: React.FC<{

const TUTORIAL_STORAGE_KEY = "shepherd-tour";

// It stores the dimension of all nodes with position as well
const [nodeDimensions, setNodeDimensions] = useState<NodeDimension>({});

useEffect(() => {
if (params.get("resetTutorial") === "true") {
localStorage.removeItem(TUTORIAL_STORAGE_KEY);
Expand Down Expand Up @@ -402,16 +424,37 @@ const FlowEditor: React.FC<{
return;
}

// Calculate the center of the viewport considering zoom
const viewportCenter = {
x: (window.innerWidth / 2 - x) / zoom,
y: (window.innerHeight / 2 - y) / zoom,
};
// What we are going to do??
// Calculate a position to the right of the newly added block, allowing for some margin.
// If adding to the right side causes the new block to collide with an existing block, attempt to place it at the bottom or left.
// Why not the top? Because the height of the new block is unknown.
// If it still collides, run a loop to find the best position where it does not collide.
// Then, adjust the canvas to center on the newly added block.
// Note: The width is known, e.g., w = 300px for a note and w = 500px for others, but the height is dynamic.

// We could also use D3 force, Intersection for this

let viewportCoordinates;
// we will get all the dimension of nodes, then store
if (nodeDimensions && Object.keys(nodeDimensions).length > 0) {
viewportCoordinates = findNewlyAddedBlockCoordinates(
nodeDimensions,
(nodeSchema.uiType == BlockUIType.NOTE ? 300 : 500) / zoom,
60 / zoom,
zoom,
);
} else {
// Calculate the center of the viewport considering zoom
viewportCoordinates = {
x: (window.innerWidth / 2 - x) / zoom,
y: (window.innerHeight / 2 - y) / zoom,
};
}

const newNode: CustomNode = {
id: nodeId.toString(),
type: "custom",
position: viewportCenter, // Set the position to the calculated viewport center
position: viewportCoordinates, // Set the position to the calculated viewport center
data: {
blockType: nodeType,
blockCosts: nodeSchema.costs,
Expand All @@ -433,6 +476,15 @@ const FlowEditor: React.FC<{
setNodeId((prevId) => prevId + 1);
clearNodesStatusAndOutput(); // Clear status and output when a new node is added

setViewport(
{
x: -viewportCoordinates.x * zoom + window.innerWidth / 2,
y: -viewportCoordinates.y * zoom + window.innerHeight / 2 - 100,
zoom: 0.8,
},
{ duration: 500 },
);

history.push({
type: "ADD_NODE",
payload: { node: { ...newNode, ...newNode.data } },
Expand All @@ -442,8 +494,10 @@ const FlowEditor: React.FC<{
},
[
nodeId,
setViewport,
availableNodes,
addNodes,
nodeDimensions,
deleteElements,
clearNodesStatusAndOutput,
x,
Expand All @@ -452,6 +506,38 @@ const FlowEditor: React.FC<{
],
);

const findNodeDimensions = useCallback(() => {
const newNodeDimensions: NodeDimension = nodes.reduce((acc, node) => {
const nodeElement = document.querySelector(
`[data-id="custom-node-${node.id}"]`,
);
if (nodeElement) {
const rect = nodeElement.getBoundingClientRect();
const { left, top, width, height } = rect;

// Convert screen coordinates to flow coordinates
const flowX = (left - x) / zoom;
const flowY = (top - y) / zoom;
const flowWidth = width / zoom;
const flowHeight = height / zoom;

acc[node.id] = {
x: flowX,
y: flowY,
width: flowWidth,
height: flowHeight,
};
}
return acc;
}, {} as NodeDimension);

setNodeDimensions(newNodeDimensions);
}, [nodes, x, y, zoom]);

useEffect(() => {
findNodeDimensions();
}, [nodes, findNodeDimensions]);

const handleUndo = () => {
history.undo();
};
Expand Down
76 changes: 76 additions & 0 deletions autogpt_platform/frontend/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { Category } from "./autogpt-server-api/types";
import { NodeDimension } from "@/components/Flow";

export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
Expand Down Expand Up @@ -213,3 +214,78 @@ export function getBehaveAs(): BehaveAs {
? BehaveAs.CLOUD
: BehaveAs.LOCAL;
}

function rectanglesOverlap(
rect1: { x: number; y: number; width: number; height?: number },
rect2: { x: number; y: number; width: number; height?: number },
): boolean {
const x1 = rect1.x,
y1 = rect1.y,
w1 = rect1.width,
h1 = rect1.height ?? 100;
const x2 = rect2.x,
y2 = rect2.y,
w2 = rect2.width,
h2 = rect2.height ?? 100;

// Check if the rectangles do not overlap
return !(x1 + w1 <= x2 || x1 >= x2 + w2 || y1 + h1 <= y2 || y1 >= y2 + h2);
}

export function findNewlyAddedBlockCoordinates(
nodeDimensions: NodeDimension,
newWidth: number,
margin: number,
zoom: number,
) {
const nodeDimensionArray = Object.values(nodeDimensions);

for (let i = nodeDimensionArray.length - 1; i >= 0; i--) {
const lastNode = nodeDimensionArray[i];
const lastNodeHeight = lastNode.height ?? 100;

// Right of the last node
let newX = lastNode.x + lastNode.width + margin;
let newY = lastNode.y;
let newRect = { x: newX, y: newY, width: newWidth, height: 100 / zoom };

const collisionRight = nodeDimensionArray.some((node) =>
rectanglesOverlap(newRect, node),
);

if (!collisionRight) {
return { x: newX, y: newY };
}

// Left of the last node
newX = lastNode.x - newWidth - margin;
newRect = { x: newX, y: newY, width: newWidth, height: 100 / zoom };

const collisionLeft = nodeDimensionArray.some((node) =>
rectanglesOverlap(newRect, node),
);

if (!collisionLeft) {
return { x: newX, y: newY };
}

// Below the last node
newX = lastNode.x;
newY = lastNode.y + lastNodeHeight + margin;
newRect = { x: newX, y: newY, width: newWidth, height: 100 / zoom };

const collisionBelow = nodeDimensionArray.some((node) =>
rectanglesOverlap(newRect, node),
);

if (!collisionBelow) {
return { x: newX, y: newY };
}
}

// Default position if no space is found
return {
x: 0,
y: 0,
};
}

0 comments on commit 9a1d0ca

Please sign in to comment.