[]) => {
+ if (!helperLineEnabled) {
+ onNodesChange(changes);
+ return;
+ }
+
+ // Apply snapping to position changes during drag
+ const modifiedChanges = changes.map((change) => {
+ if (
+ change.type === "position" &&
+ "dragging" in change &&
+ "position" in change &&
+ "id" in change &&
+ isDragging
+ ) {
+ const nodeId = change.id as string;
+ const draggedNode = nodes.find((n) => n.id === nodeId);
+
+ if (draggedNode && change.position) {
+ const updatedNode = {
+ ...draggedNode,
+ position: change.position,
+ };
+
+ const snapPosition = getSnapPosition(updatedNode, nodes);
+
+ // Only snap if we're actively dragging
+ if (change.dragging) {
+ // Apply snap if there's a significant difference
+ if (
+ Math.abs(snapPosition.x - change.position.x) > 0.1 ||
+ Math.abs(snapPosition.y - change.position.y) > 0.1
+ ) {
+ return {
+ ...change,
+ position: snapPosition,
+ };
+ }
+ } else {
+ // This is the final position change when drag ends
+ // Force snap to ensure it stays where it should
+ return {
+ ...change,
+ position: snapPosition,
+ };
+ }
+ }
+ }
+ return change;
+ });
+
+ onNodesChange(modifiedChanges);
+ },
+ [onNodesChange, nodes, isDragging, helperLineEnabled],
+ );
const onSelectionDragStart: SelectionDragHandler = useCallback(() => {
- // 👇 make dragging a selection undoable
-
takeSnapshot();
}, [takeSnapshot]);
@@ -596,7 +682,7 @@ export default function Page({
nodes={nodes}
edges={edges}
- onNodesChange={onNodesChange}
+ onNodesChange={onNodesChangeWithHelperLines}
onEdgesChange={onEdgesChange}
onConnect={isLocked ? undefined : onConnectMod}
disableKeyboardA11y={true}
@@ -605,6 +691,7 @@ export default function Page({
onReconnect={isLocked ? undefined : onEdgeUpdate}
onReconnectStart={isLocked ? undefined : onEdgeUpdateStart}
onReconnectEnd={isLocked ? undefined : onEdgeUpdateEnd}
+ onNodeDrag={onNodeDrag}
onNodeDragStart={onNodeDragStart}
onSelectionDragStart={onSelectionDragStart}
elevateEdgesOnSelect={true}
@@ -634,6 +721,7 @@ export default function Page({
+ {helperLineEnabled && }
((set, get) => ({
);
set({ dismissedNodes: newDismissedNodes });
},
+ helperLineEnabled: false,
+ setHelperLineEnabled: (helperLineEnabled: boolean) => {
+ set({ helperLineEnabled });
+ },
setNewChatOnPlayground: (newChat: boolean) => {
set({ newChatOnPlayground: newChat });
},
diff --git a/src/frontend/src/style/applies.css b/src/frontend/src/style/applies.css
index ac28563f2..4de67ddac 100644
--- a/src/frontend/src/style/applies.css
+++ b/src/frontend/src/style/applies.css
@@ -1270,6 +1270,26 @@
--tw-ring-offset-shadow: none !important;
--tw-ring-color: none !important;
}
+
+ .helper-lines {
+ @apply pointer-events-none absolute left-0 top-0 z-10 h-full w-full;
+ }
+
+ .helper-line {
+ stroke: hsl(var(--primary));
+ stroke-width: 1;
+ stroke-dasharray: 4 4;
+ opacity: 0.8;
+ filter: drop-shadow(0 0 2px hsl(var(--primary) / 0.3));
+ }
+
+ .helper-line.horizontal {
+ stroke: hsl(var(--primary));
+ }
+
+ .helper-line.vertical {
+ stroke: hsl(var(--primary));
+ }
}
/* Gradient background */
diff --git a/src/frontend/src/types/zustand/flow/index.ts b/src/frontend/src/types/zustand/flow/index.ts
index 1ce0907f4..a6cb473c1 100644
--- a/src/frontend/src/types/zustand/flow/index.ts
+++ b/src/frontend/src/types/zustand/flow/index.ts
@@ -285,6 +285,8 @@ export type FlowStoreType = {
setCurrentBuildingNodeId: (nodeIds: string[] | undefined) => void;
clearEdgesRunningByNodes: () => Promise;
updateToolMode: (nodeId: string, toolMode: boolean) => void;
+ helperLineEnabled: boolean;
+ setHelperLineEnabled: (helperLineEnabled: boolean) => void;
newChatOnPlayground: boolean;
setNewChatOnPlayground: (newChat: boolean) => void;
};