11import { Path , Group , Circle , RegularPolygon } from "react-konva" ;
2- import { useState , useRef } from "react" ;
2+ import { useState , useRef , useMemo } from "react" ;
3+
4+ import type { XYPosition } from "@/types/positions" ;
35
46import eventEmitter from "@/events-emitter" ;
57import { tableCoordsStore } from "@/stores/tableCoords" ;
@@ -58,6 +60,62 @@ const ConnectionPath = ({
5860
5961 const colsIndexes = tablesInfo . colsIndexes ?? { } ;
6062
63+ const countFieldsForTable = ( tableName : string ) : number =>
64+ Object . keys ( colsIndexes ) . filter ( ( k ) => k . startsWith ( `${ tableName } .` ) )
65+ . length ;
66+
67+ const [ sourceFieldsCount , targetFieldsCount ] = useMemo (
68+ ( ) => [
69+ countFieldsForTable ( sourceTableName ) ,
70+ countFieldsForTable ( targetTableName ) ,
71+ ] ,
72+ [ colsIndexes , sourceTableName , targetTableName ] ,
73+ ) ;
74+
75+ const srcHeight = TABLE_HEADER_HEIGHT + COLUMN_HEIGHT * sourceFieldsCount ;
76+ const tgtHeight = TABLE_HEADER_HEIGHT + COLUMN_HEIGHT * targetFieldsCount ;
77+
78+ interface TableBounds {
79+ left : number ;
80+ right : number ;
81+ top : number ;
82+ bottom : number ;
83+ }
84+
85+ const buildTableBounds = (
86+ coords : XYPosition ,
87+ width : number ,
88+ height : number ,
89+ ) : TableBounds => ( {
90+ left : coords . x ?? 0 ,
91+ right : ( coords . x ?? 0 ) + width ,
92+ top : coords . y ?? 0 ,
93+ bottom : ( coords . y ?? 0 ) + height ,
94+ } ) ;
95+
96+ const distanceToBounds = ( point : XYPosition , bounds : TableBounds ) : number => {
97+ const clampedX = Math . max ( bounds . left , Math . min ( point . x , bounds . right ) ) ;
98+ const clampedY = Math . max ( bounds . top , Math . min ( point . y , bounds . bottom ) ) ;
99+ return Math . hypot ( point . x - clampedX , point . y - clampedY ) ;
100+ } ;
101+
102+ const resolveTargetByEdgeDistance = (
103+ point : XYPosition ,
104+ srcCoords : XYPosition ,
105+ tgtCoords : XYPosition ,
106+ ) : string => {
107+ const sourceBounds = buildTableBounds ( srcCoords , srcWidth , srcHeight ) ;
108+ const targetBounds = buildTableBounds ( tgtCoords , tgtWidth , tgtHeight ) ;
109+ const sourceDistance = distanceToBounds ( point , sourceBounds ) ;
110+ const targetDistance = distanceToBounds ( point , targetBounds ) ;
111+ return sourceDistance < targetDistance ? targetTableName : sourceTableName ;
112+ } ;
113+
114+ const normalizeAngle = ( angle : number ) : number => {
115+ const normalized = angle % 360 ;
116+ return normalized < 0 ? normalized + 360 : normalized ;
117+ } ;
118+
61119 const highlight =
62120 hoveredTableName === sourceTableName ||
63121 hoveredTableName === targetTableName ||
@@ -155,8 +213,14 @@ const ConnectionPath = ({
155213 END_LINE_TOLERATE ,
156214 ) ;
157215
158- setBtnPos ( { x : localX , y : localY } ) ;
159- if ( computedArrow != null ) setBtnTarget ( computedArrow ) ;
216+ const buttonPoint : XYPosition = { x : localX , y : localY } ;
217+ setBtnPos ( buttonPoint ) ;
218+ const edgeTarget = resolveTargetByEdgeDistance (
219+ buttonPoint ,
220+ srcCoords ,
221+ tgtCoords ,
222+ ) ;
223+ setBtnTarget ( edgeTarget ) ;
160224
161225 // Ensure arrow is above relation lines but below table nodes. We look for
162226 // the first child whose name starts with `table-` and set the arrow's
@@ -165,15 +229,25 @@ const ConnectionPath = ({
165229 if ( ! tooClose && arrowGroupRef . current != null ) {
166230 setArrowZIndexRelativeToTables ( arrowGroupRef . current ) ;
167231 }
168- if ( computedAngle != null ) setBtnAngle ( computedAngle + ARROW_ANGLE_OFFSET ) ;
232+ if ( computedAngle != null ) {
233+ let finalAngle = computedAngle ;
234+ if (
235+ computedArrow != null &&
236+ edgeTarget != null &&
237+ edgeTarget !== computedArrow
238+ ) {
239+ finalAngle += 180 ;
240+ }
241+ setBtnAngle ( normalizeAngle ( finalAngle ) + ARROW_ANGLE_OFFSET ) ;
242+ }
169243
170244 setBtnVisible ( ! tooClose ) ;
171245 } ;
172246
173247 const handleBtnLeave = ( ) => {
174248 document . body . style . cursor = "default" ;
175249 setBtnHovering ( false ) ;
176- setIsHovered ( false ) ; // unhover both line and circle
250+ setIsHovered ( false ) ;
177251 if ( hideTimeoutRef . current != null ) {
178252 window . clearTimeout ( hideTimeoutRef . current ) ;
179253 }
@@ -201,38 +275,7 @@ const ConnectionPath = ({
201275 const btnLocal = btnPos ;
202276 let targetToUse : string | null = btnTarget ;
203277 if ( btnLocal != null ) {
204- // Derive visible fields count from colsIndexes in tablesInfo
205- const colsIndexes = tablesInfo . colsIndexes ?? { } ;
206- const countKeysWithPrefix = ( prefix : string ) =>
207- Object . keys ( colsIndexes ) . filter ( ( k ) => k . startsWith ( `${ prefix } .` ) )
208- . length ;
209-
210- const srcFields = countKeysWithPrefix ( sourceTableName ) ;
211- const tgtFields = countKeysWithPrefix ( targetTableName ) ;
212-
213- const srcHeight = TABLE_HEADER_HEIGHT + COLUMN_HEIGHT * srcFields ;
214- const tgtHeight = TABLE_HEADER_HEIGHT + COLUMN_HEIGHT * tgtFields ;
215-
216- const srcCenter = {
217- x : ( srcCoords . x ?? 0 ) + srcWidth / 2 ,
218- y : ( srcCoords . y ?? 0 ) + srcHeight / 2 ,
219- } ;
220- const tgtCenter = {
221- x : ( tgtCoords . x ?? 0 ) + tgtWidth / 2 ,
222- y : ( tgtCoords . y ?? 0 ) + tgtHeight / 2 ,
223- } ;
224-
225- const ds = Math . hypot (
226- ( btnLocal . x ?? 0 ) - srcCenter . x ,
227- ( btnLocal . y ?? 0 ) - srcCenter . y ,
228- ) ;
229- const dt = Math . hypot (
230- ( btnLocal . x ?? 0 ) - tgtCenter . x ,
231- ( btnLocal . y ?? 0 ) - tgtCenter . y ,
232- ) ;
233-
234- // Farther table wins
235- targetToUse = ds < dt ? targetTableName : sourceTableName ;
278+ targetToUse = resolveTargetByEdgeDistance ( btnLocal , srcCoords , tgtCoords ) ;
236279 }
237280
238281 if ( targetToUse == null || targetToUse . length === 0 ) return ;
0 commit comments