feat: integrate centered flow node positioning in canvas components
- Added useCenteredFlowNodePosition hook to improve node placement in the CanvasCommandPalette and CanvasToolbar. - Updated node creation logic to utilize centered positioning, enhancing the visual layout and user experience during node interactions. - Refactored offset calculations to stagger node positions based on the current node count, ensuring a more organized canvas layout.
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import { useCanvasPlacement } from "@/components/canvas/canvas-placement-context";
|
import { useCanvasPlacement } from "@/components/canvas/canvas-placement-context";
|
||||||
|
import { useCenteredFlowNodePosition } from "@/hooks/use-centered-flow-node-position";
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
CommandDialog,
|
CommandDialog,
|
||||||
@@ -53,6 +54,7 @@ const NODE_SEARCH_KEYWORDS: Partial<
|
|||||||
export function CanvasCommandPalette() {
|
export function CanvasCommandPalette() {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const { createNodeWithIntersection } = useCanvasPlacement();
|
const { createNodeWithIntersection } = useCanvasPlacement();
|
||||||
|
const getCenteredPosition = useCenteredFlowNodePosition();
|
||||||
const { setTheme } = useTheme();
|
const { setTheme } = useTheme();
|
||||||
const nodeCountRef = useRef(0);
|
const nodeCountRef = useRef(0);
|
||||||
|
|
||||||
@@ -73,12 +75,12 @@ export function CanvasCommandPalette() {
|
|||||||
width: number,
|
width: number,
|
||||||
height: number,
|
height: number,
|
||||||
) => {
|
) => {
|
||||||
const offset = (nodeCountRef.current % 8) * 24;
|
const stagger = (nodeCountRef.current % 8) * 24;
|
||||||
nodeCountRef.current += 1;
|
nodeCountRef.current += 1;
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
void createNodeWithIntersection({
|
void createNodeWithIntersection({
|
||||||
type,
|
type,
|
||||||
position: { x: 100 + offset, y: 100 + offset },
|
position: getCenteredPosition(width, height, stagger),
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
data,
|
data,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useRef } from "react";
|
|||||||
import { CreditDisplay } from "@/components/canvas/credit-display";
|
import { CreditDisplay } from "@/components/canvas/credit-display";
|
||||||
import { ExportButton } from "@/components/canvas/export-button";
|
import { ExportButton } from "@/components/canvas/export-button";
|
||||||
import { useCanvasPlacement } from "@/components/canvas/canvas-placement-context";
|
import { useCanvasPlacement } from "@/components/canvas/canvas-placement-context";
|
||||||
|
import { useCenteredFlowNodePosition } from "@/hooks/use-centered-flow-node-position";
|
||||||
import {
|
import {
|
||||||
CANVAS_NODE_TEMPLATES,
|
CANVAS_NODE_TEMPLATES,
|
||||||
type CanvasNodeTemplate,
|
type CanvasNodeTemplate,
|
||||||
@@ -18,6 +19,7 @@ export default function CanvasToolbar({
|
|||||||
canvasName,
|
canvasName,
|
||||||
}: CanvasToolbarProps) {
|
}: CanvasToolbarProps) {
|
||||||
const { createNodeWithIntersection } = useCanvasPlacement();
|
const { createNodeWithIntersection } = useCanvasPlacement();
|
||||||
|
const getCenteredPosition = useCenteredFlowNodePosition();
|
||||||
const nodeCountRef = useRef(0);
|
const nodeCountRef = useRef(0);
|
||||||
|
|
||||||
const handleAddNode = async (
|
const handleAddNode = async (
|
||||||
@@ -26,11 +28,11 @@ export default function CanvasToolbar({
|
|||||||
width: number,
|
width: number,
|
||||||
height: number,
|
height: number,
|
||||||
) => {
|
) => {
|
||||||
const offset = (nodeCountRef.current % 8) * 24;
|
const stagger = (nodeCountRef.current % 8) * 24;
|
||||||
nodeCountRef.current += 1;
|
nodeCountRef.current += 1;
|
||||||
await createNodeWithIntersection({
|
await createNodeWithIntersection({
|
||||||
type,
|
type,
|
||||||
position: { x: 100 + offset, y: 100 + offset },
|
position: getCenteredPosition(width, height, stagger),
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
data,
|
data,
|
||||||
|
|||||||
44
hooks/use-centered-flow-node-position.ts
Normal file
44
hooks/use-centered-flow-node-position.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import { useReactFlow } from "@xyflow/react";
|
||||||
|
|
||||||
|
const SNAP = 16;
|
||||||
|
|
||||||
|
function snapToGrid(x: number, y: number): { x: number; y: number } {
|
||||||
|
return {
|
||||||
|
x: Math.round(x / SNAP) * SNAP,
|
||||||
|
y: Math.round(y / SNAP) * SNAP,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Top-left flow position for a node centered in the visible React Flow pane
|
||||||
|
* (viewport), with optional stagger and 16px grid snap to match the canvas.
|
||||||
|
*/
|
||||||
|
export function useCenteredFlowNodePosition() {
|
||||||
|
const { screenToFlowPosition } = useReactFlow();
|
||||||
|
|
||||||
|
return useCallback(
|
||||||
|
(width: number, height: number, stagger: number) => {
|
||||||
|
const pane = document.querySelector(".react-flow__pane");
|
||||||
|
const rect =
|
||||||
|
pane?.getBoundingClientRect() ??
|
||||||
|
document.querySelector(".react-flow")?.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (!rect || rect.width === 0 || rect.height === 0) {
|
||||||
|
return snapToGrid(100 + stagger, 100 + stagger);
|
||||||
|
}
|
||||||
|
|
||||||
|
const center = screenToFlowPosition({
|
||||||
|
x: rect.left + rect.width / 2,
|
||||||
|
y: rect.top + rect.height / 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
const x = center.x - width / 2 + stagger;
|
||||||
|
const y = center.y - height / 2 + stagger;
|
||||||
|
return snapToGrid(x, y);
|
||||||
|
},
|
||||||
|
[screenToFlowPosition],
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user