11import { Node } from "@xyflow/react"
22import { AlignmentGuide } from "@/store/alignmentGuidesStore"
3+ import { getPositionOnCanvas , isParentNodeType } from "@/utils/nodeUtils"
34
45const ALIGNMENT_THRESHOLD = 10 // pixels within which guides appear
56
@@ -15,9 +16,12 @@ export type AlignmentInfo = {
1516/**
1617 * Get the bounds of a node (considering its position and dimensions)
1718 */
18- export const getNodeBounds = ( node : Node ) => {
19- const x = node . position . x
20- const y = node . position . y
19+ export const getNodeBounds = ( node : Node , allNodes ?: Node [ ] ) => {
20+ const position = allNodes
21+ ? getPositionOnCanvas ( node , allNodes )
22+ : node . position
23+ const x = position . x
24+ const y = position . y
2125 const width = node . measured ?. width || 100
2226 const height = node . measured ?. height || 100
2327
@@ -31,6 +35,78 @@ export const getNodeBounds = (node: Node) => {
3135 }
3236}
3337
38+ const getContainingParentId = (
39+ node : Node ,
40+ allNodes : Node [ ] ,
41+ excludeParentId ?: string
42+ ) => {
43+ if ( node . parentId ) {
44+ return node . parentId
45+ }
46+
47+ const nodeBounds = getNodeBounds ( node , allNodes )
48+ let bestParent : Node | undefined
49+ let bestArea = Infinity
50+
51+ for ( const candidate of allNodes ) {
52+ if (
53+ candidate . id === node . id ||
54+ candidate . id === excludeParentId ||
55+ ! isParentNodeType ( candidate . type )
56+ ) {
57+ continue
58+ }
59+
60+ const parentBounds = getNodeBounds ( candidate , allNodes )
61+ const contains =
62+ nodeBounds . left >= parentBounds . left &&
63+ nodeBounds . right <= parentBounds . right &&
64+ nodeBounds . top >= parentBounds . top &&
65+ nodeBounds . bottom <= parentBounds . bottom
66+
67+ if ( ! contains ) {
68+ continue
69+ }
70+
71+ const area =
72+ ( parentBounds . right - parentBounds . left ) *
73+ ( parentBounds . bottom - parentBounds . top )
74+ if ( area < bestArea ) {
75+ bestArea = area
76+ bestParent = candidate
77+ }
78+ }
79+
80+ return bestParent ?. id
81+ }
82+
83+ const shouldUseAsGuideTarget = (
84+ draggedNode : Node ,
85+ node : Node ,
86+ allNodes : Node [ ] ,
87+ draggedParentId : string | undefined
88+ ) => {
89+ const nodeParentId = getContainingParentId ( node , allNodes , draggedNode . id )
90+
91+ if ( ! draggedParentId ) {
92+ // Dragged node is top-level: align with everything except nodes that
93+ // are children of a parent the dragged node has no relation to...
94+ // actually align with all nodes including parent frames
95+ return true
96+ }
97+
98+ // Dragged node is a child: skip the parent container it belongs to
99+ // (no point snapping to your own frame), but allow all other nodes
100+ if ( node . id === draggedParentId ) {
101+ return false
102+ }
103+
104+ const isSibling = nodeParentId === draggedParentId
105+ const isTopLevel = ! nodeParentId
106+
107+ return isSibling || isTopLevel
108+ }
109+
34110/**
35111 * Calculate alignment guides based on dragged node and other nodes
36112 */
@@ -39,14 +115,22 @@ export const calculateAlignmentGuides = (
39115 allNodes : Node [ ] ,
40116 threshold : number = ALIGNMENT_THRESHOLD
41117) : AlignmentGuide [ ] => {
42- const draggedBounds = getNodeBounds ( draggedNode )
118+ const nodesWithDrag = allNodes . map ( ( node ) =>
119+ node . id === draggedNode . id ? draggedNode : node
120+ )
121+ const draggedParentId = draggedNode . parentId
122+ const draggedBounds = getNodeBounds ( draggedNode , nodesWithDrag )
43123 const guides : AlignmentGuide [ ] = [ ]
44124 const alignedPositions = new Set < number > ( )
45125
46- const otherNodes = allNodes . filter ( ( n ) => n . id !== draggedNode . id )
126+ const otherNodes = nodesWithDrag . filter (
127+ ( node ) =>
128+ node . id !== draggedNode . id &&
129+ shouldUseAsGuideTarget ( draggedNode , node , nodesWithDrag , draggedParentId )
130+ )
47131
48132 for ( const node of otherNodes ) {
49- const nodeBounds = getNodeBounds ( node )
133+ const nodeBounds = getNodeBounds ( node , nodesWithDrag )
50134
51135 // Vertical alignment (left, center, right edges)
52136 const verticalAlignments = [
@@ -139,48 +223,52 @@ export const calculateAlignmentGuides = (
139223export const snapNodeToGuides = (
140224 draggedNode : Node ,
141225 guides : AlignmentGuide [ ] ,
142- threshold : number = ALIGNMENT_THRESHOLD
226+ threshold : number = ALIGNMENT_THRESHOLD ,
227+ allNodes ?: Node [ ]
143228) : { x ?: number ; y ?: number } => {
144- const draggedBounds = getNodeBounds ( draggedNode )
229+ const draggedBounds = getNodeBounds ( draggedNode , allNodes )
230+ const draggedPosition = allNodes
231+ ? getPositionOnCanvas ( draggedNode , allNodes )
232+ : draggedNode . position
145233 const snappedPosition : { x ?: number ; y ?: number } = { }
146234
147235 for ( const guide of guides ) {
148236 if ( guide . type === "vertical" ) {
149237 // Snap left edge
150238 if ( Math . abs ( draggedBounds . left - guide . position ) < threshold ) {
151- snappedPosition . x = guide . position - draggedNode . position . x
239+ snappedPosition . x = guide . position - draggedPosition . x
152240 }
153241 // Snap center
154242 if ( Math . abs ( draggedBounds . centerX - guide . position ) < threshold ) {
155243 snappedPosition . x =
156244 guide . position -
157- draggedNode . position . x -
245+ draggedPosition . x -
158246 ( draggedNode . measured ?. width || 100 ) / 2
159247 }
160248 // Snap right edge
161249 if ( Math . abs ( draggedBounds . right - guide . position ) < threshold ) {
162250 snappedPosition . x =
163251 guide . position -
164- draggedNode . position . x -
252+ draggedPosition . x -
165253 ( draggedNode . measured ?. width || 100 )
166254 }
167255 } else if ( guide . type === "horizontal" ) {
168256 // Snap top edge
169257 if ( Math . abs ( draggedBounds . top - guide . position ) < threshold ) {
170- snappedPosition . y = guide . position - draggedNode . position . y
258+ snappedPosition . y = guide . position - draggedPosition . y
171259 }
172260 // Snap center
173261 if ( Math . abs ( draggedBounds . centerY - guide . position ) < threshold ) {
174262 snappedPosition . y =
175263 guide . position -
176- draggedNode . position . y -
264+ draggedPosition . y -
177265 ( draggedNode . measured ?. height || 100 ) / 2
178266 }
179267 // Snap bottom edge
180268 if ( Math . abs ( draggedBounds . bottom - guide . position ) < threshold ) {
181269 snappedPosition . y =
182270 guide . position -
183- draggedNode . position . y -
271+ draggedPosition . y -
184272 ( draggedNode . measured ?. height || 100 )
185273 }
186274 }
0 commit comments