1- import { Path } from "react-konva" ;
2- import { useState } from "react" ;
1+ import { Path , Group , Circle , RegularPolygon } from "react-konva" ;
2+ import { useState , useRef } from "react" ;
33
4+ import eventEmitter from "@/events-emitter" ;
5+ import { tableCoordsStore } from "@/stores/tableCoords" ;
6+ import {
7+ DIAGRAM_PADDING ,
8+ COLUMN_HEIGHT ,
9+ TABLE_HEADER_HEIGHT ,
10+ } from "@/constants/sizing" ;
411import { useThemeColors } from "@/hooks/theme" ;
512import { useTablesInfo } from "@/hooks/table" ;
613import { useTableColor } from "@/hooks/tableColor" ;
14+ import { useTableWidthStoredValue } from "@/hooks/tableWidthStore" ;
15+ import {
16+ findNearestPointOnPath ,
17+ getButtonLocalAndStage ,
18+ decideTargetAndAngleFromPath ,
19+ computeTooCloseStage ,
20+ setArrowZIndexRelativeToTables ,
21+ } from "@/utils/connectionPathUtils" ;
722
823interface ConnectionPathProps {
924 path : string ;
1025 sourceTableName : string ;
1126 targetTableName : string ;
1227 relationOwner : string ;
1328}
29+
30+ const ARROW_ANGLE_OFFSET = 90 ;
31+ const MIN_LINE_OFFSET = 40 ; // Distance from each endpoint - hides circle if within this distance
32+ const END_LINE_TOLERATE = 5 ; // At the end of the target line, it needs hardcoded additional distance
33+ const ARROW_BUTTON_DISAPPEARANCE_DELAY = 50 ;
34+
1435const ConnectionPath = ( {
1536 path,
1637 sourceTableName,
1738 targetTableName,
1839 relationOwner,
1940} : ConnectionPathProps ) => {
2041 const themeColors = useThemeColors ( ) ;
21- const { hoveredTableName } = useTablesInfo ( ) ;
42+ const tablesInfo = useTablesInfo ( ) ;
43+ const { hoveredTableName, setHoveredTableName } = tablesInfo ;
2244 const sourceTableColors = useTableColor ( relationOwner ) ;
45+ const srcWidth = useTableWidthStoredValue ( sourceTableName ) ;
46+ const tgtWidth = useTableWidthStoredValue ( targetTableName ) ;
2347 const [ isHovered , setIsHovered ] = useState ( false ) ;
48+ const [ btnVisible , setBtnVisible ] = useState ( false ) ;
49+ const [ btnPos , setBtnPos ] = useState ( { x : 0 , y : 0 } ) ;
50+ const [ btnAngle , setBtnAngle ] = useState ( 0 ) ;
51+ const [ btnTarget , setBtnTarget ] = useState < string | null > ( null ) ;
52+ const [ btnHovering , setBtnHovering ] = useState ( false ) ;
53+ const hideTimeoutRef = useRef < number | null > ( null ) ;
54+ const btnStagePosRef = useRef < { x : number ; y : number } | null > ( null ) ;
55+
56+ // Keep a ref to the arrow group so we can bring it to top of the layer when visible
57+ const arrowGroupRef = useRef < any > ( null ) ;
58+
59+ const colsIndexes = tablesInfo . colsIndexes ?? { } ;
2460
2561 const highlight =
2662 hoveredTableName === sourceTableName ||
@@ -33,20 +69,220 @@ const ConnectionPath = ({
3369
3470 const handleOnHover = ( ) => {
3571 setIsHovered ( true ) ;
72+ setBtnVisible ( true ) ;
3673 } ;
3774
3875 const handleOnBlur = ( ) => {
3976 setIsHovered ( false ) ;
77+ if ( hideTimeoutRef . current != null ) {
78+ window . clearTimeout ( hideTimeoutRef . current ) ;
79+ }
80+ hideTimeoutRef . current = window . setTimeout ( ( ) => {
81+ if ( ! btnHovering ) setBtnVisible ( false ) ;
82+ hideTimeoutRef . current = null ;
83+ } , ARROW_BUTTON_DISAPPEARANCE_DELAY ) ;
84+ } ;
85+
86+ const handleOnMove = ( e : any ) => {
87+ const stage = e . target . getStage ( ) ;
88+ if ( stage == null ) return ;
89+ const pointer = stage . getPointerPosition ( ) ;
90+ if ( pointer == null ) return ;
91+
92+ const px = pointer . x as number ;
93+ const py = pointer . y as number ;
94+
95+ const node = e . target ;
96+ const srcCoords = tableCoordsStore . getCoords ( sourceTableName ) ;
97+ const tgtCoords = tableCoordsStore . getCoords ( targetTableName ) ;
98+ let localX = px ;
99+ let localY = py ;
100+
101+ // Local computed results
102+ let computedArrow : string | null = null ;
103+ let computedAngle : number | null = null ;
104+
105+ try {
106+ const { best, bestL, totalLength } = findNearestPointOnPath ( node , px , py ) ;
107+ if ( best != null && totalLength > 0 ) {
108+ const {
109+ localX : lX ,
110+ localY : lY ,
111+ btnStagePos,
112+ } = getButtonLocalAndStage ( node , best , DIAGRAM_PADDING ) ;
113+ localX = lX ;
114+ localY = lY ;
115+ if ( btnStagePos != null ) btnStagePosRef . current = btnStagePos ;
116+
117+ // Decide arrow target and angle based on path geometry + stage centers
118+ try {
119+ const decision = decideTargetAndAngleFromPath (
120+ node ,
121+ bestL ,
122+ totalLength ,
123+ btnStagePosRef . current ,
124+ sourceTableName ,
125+ targetTableName ,
126+ colsIndexes ,
127+ srcWidth ,
128+ tgtWidth ,
129+ srcCoords ,
130+ tgtCoords ,
131+ ) ;
132+ if ( decision . computedArrow != null )
133+ computedArrow = decision . computedArrow ;
134+ if ( decision . computedAngle != null )
135+ computedAngle = decision . computedAngle ; // degrees without ARROW_ANGLE_OFFSET
136+ } catch ( ex ) {
137+ // Ignore and fallback below
138+ }
139+ }
140+ } catch ( err ) {
141+ // Fallback to stage coords + padding
142+ localX = px + DIAGRAM_PADDING ;
143+ localY = py + DIAGRAM_PADDING ;
144+ btnStagePosRef . current = { x : px , y : py } ;
145+ }
146+
147+ // Decide visibility: hide when too close to path endpoints (prevents overlap with tables and relation markers).
148+ // Use a robust stage-space distance check between the current button
149+ // position and the path start/end. This is simpler and more reliable than
150+ // depending on arc-length methods which can be flaky on transformed paths.
151+ const tooClose = computeTooCloseStage (
152+ node ,
153+ btnStagePosRef . current ?? { x : px , y : py } ,
154+ MIN_LINE_OFFSET ,
155+ END_LINE_TOLERATE ,
156+ ) ;
157+
158+ setBtnPos ( { x : localX , y : localY } ) ;
159+ if ( computedArrow != null ) setBtnTarget ( computedArrow ) ;
160+
161+ // Ensure arrow is above relation lines but below table nodes. We look for
162+ // the first child whose name starts with `table-` and set the arrow's
163+ // z-index to just before that index. If no table child is found, move the
164+ // arrow to the top of the parent's children so it's above lines.
165+ if ( ! tooClose && arrowGroupRef . current != null ) {
166+ setArrowZIndexRelativeToTables ( arrowGroupRef . current ) ;
167+ }
168+ if ( computedAngle != null ) setBtnAngle ( computedAngle + ARROW_ANGLE_OFFSET ) ;
169+
170+ setBtnVisible ( ! tooClose ) ;
171+ } ;
172+
173+ const handleBtnLeave = ( ) => {
174+ document . body . style . cursor = "default" ;
175+ setBtnHovering ( false ) ;
176+ setIsHovered ( false ) ; // unhover both line and circle
177+ if ( hideTimeoutRef . current != null ) {
178+ window . clearTimeout ( hideTimeoutRef . current ) ;
179+ }
180+ hideTimeoutRef . current = window . setTimeout ( ( ) => {
181+ setBtnVisible ( false ) ;
182+ hideTimeoutRef . current = null ;
183+ } , 80 ) ;
184+ } ;
185+
186+ const handleBtnEnter = ( ) => {
187+ document . body . style . cursor = "pointer" ;
188+ setBtnHovering ( true ) ;
189+ setIsHovered ( true ) ; // hover both line and circle
190+ if ( hideTimeoutRef . current != null ) {
191+ window . clearTimeout ( hideTimeoutRef . current ) ;
192+ hideTimeoutRef . current = null ;
193+ }
194+ } ;
195+
196+ const handleBtnClick = ( ) => {
197+ // Recompute the intended target using the stored stage-position of the button
198+ const srcCoords = tableCoordsStore . getCoords ( sourceTableName ) ;
199+ const tgtCoords = tableCoordsStore . getCoords ( targetTableName ) ;
200+ // Use the button position in the same coordinate-space as table coords (btnPos)
201+ const btnLocal = btnPos ;
202+ let targetToUse : string | null = btnTarget ;
203+ 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 ;
236+ }
237+
238+ if ( targetToUse == null || targetToUse . length === 0 ) return ;
239+ try {
240+ setHoveredTableName ( targetToUse ) ;
241+ } catch ( e ) {
242+ // Ignore
243+ }
244+ eventEmitter . emit ( "table:center" , { tableName : targetToUse } ) ;
245+ eventEmitter . emit ( `highlight:table:${ targetToUse } ` ) ;
40246 } ;
41247
42248 return (
43- < Path
44- data = { path }
45- onMouseEnter = { handleOnHover }
46- onMouseLeave = { handleOnBlur }
47- strokeWidth = { 2 }
48- stroke = { strokeColor }
49- />
249+ < >
250+ < Path
251+ data = { path }
252+ onMouseEnter = { handleOnHover }
253+ onMouseLeave = { handleOnBlur }
254+ onMouseMove = { handleOnMove }
255+ hitStrokeWidth = { 12 }
256+ strokeWidth = { 2 }
257+ stroke = { strokeColor }
258+ />
259+
260+ { btnVisible && (
261+ < Group x = { btnPos . x } y = { btnPos . y } listening = { true } ref = { arrowGroupRef } >
262+ < Circle
263+ x = { 0 }
264+ y = { 0 }
265+ radius = { 14 }
266+ fill = { strokeColor }
267+ cursor = "pointer"
268+ onMouseEnter = { handleBtnEnter }
269+ onMouseLeave = { handleBtnLeave }
270+ onClick = { handleBtnClick }
271+ listening = { true }
272+ />
273+ { /* Triangle arrow as vector, rotated to face target */ }
274+ < RegularPolygon
275+ x = { 0 }
276+ y = { 0 }
277+ sides = { 3 }
278+ radius = { 6 }
279+ fill = "#fff"
280+ rotation = { btnAngle }
281+ listening = { false }
282+ />
283+ </ Group >
284+ ) }
285+ </ >
50286 ) ;
51287} ;
52288
0 commit comments