1- import { useState , useRef , useEffect } from 'react' ;
1+ import { useState , useRef , useEffect , useCallback } from 'react' ;
22import { endpoints } from '../services/apiEndpoints' ;
33
4+ /**
5+ * Hook for bounding-box drag drawing (classic tracker) and smart-click (AI tracker).
6+ *
7+ * Uses Pointer Events with pointer capture so the rectangle tracks cleanly
8+ * even when the cursor/finger leaves the video container during a drag.
9+ */
410const useBoundingBoxHandlers = ( isTracking , setIsTracking , smartModeActive = false ) => {
511 const [ startPos , setStartPos ] = useState ( null ) ;
612 const [ currentPos , setCurrentPos ] = useState ( null ) ;
713 const [ boundingBox , setBoundingBox ] = useState ( null ) ;
8- const imageRef = useRef ( ) ;
14+ const imageRef = useRef ( null ) ;
915
1016 const defaultBoundingBoxSize =
1117 parseFloat ( process . env . REACT_APP_DEFAULT_BOUNDING_BOX_SIZE ) || 0.2 ;
1218
1319 const timeoutRef = useRef ( null ) ;
20+ const draggingRef = useRef ( false ) ;
1421
1522 useEffect ( ( ) => {
1623 return ( ) => {
17- if ( timeoutRef . current ) {
18- clearTimeout ( timeoutRef . current ) ;
19- }
24+ if ( timeoutRef . current ) clearTimeout ( timeoutRef . current ) ;
2025 } ;
2126 } , [ ] ) ;
2227
@@ -26,7 +31,7 @@ const useBoundingBoxHandlers = (isTracking, setIsTracking, smartModeActive = fal
2631 return Math . sqrt ( dx * dx + dy * dy ) ;
2732 } ;
2833
29- const startTracking = async ( bbox ) => {
34+ const startTracking = useCallback ( async ( bbox ) => {
3035 try {
3136 if ( isTracking ) {
3237 await fetch ( endpoints . stopTracking , {
@@ -46,60 +51,65 @@ const useBoundingBoxHandlers = (isTracking, setIsTracking, smartModeActive = fal
4651 } catch ( error ) {
4752 console . error ( 'Error:' , error ) ;
4853 }
49- } ;
54+ } , [ isTracking , setIsTracking ] ) ;
5055
51- const sendSmartClick = async ( normX , normY ) => {
52- try {
53- const res = await fetch ( endpoints . smartClick , {
54- method : 'POST' ,
55- headers : { 'Content-Type' : 'application/json' } ,
56- body : JSON . stringify ( { x : normX , y : normY } ) ,
57- } ) ;
58- const data = await res . json ( ) ;
59- console . log ( 'Smart click sent:' , data ) ;
60- } catch ( err ) {
61- console . error ( 'Failed to send smart click:' , err ) ;
62- }
63- } ;
56+ // ── Pointer handlers (unified mouse + touch + pen) ─────────────────
57+
58+ const handlePointerDown = useCallback ( ( e ) => {
59+ if ( smartModeActive ) return ; // smart mode uses onClick instead
60+ if ( e . button !== 0 ) return ; // left button only
61+
62+ e . preventDefault ( ) ;
63+ e . target . setPointerCapture ( e . pointerId ) ;
64+ draggingRef . current = true ;
6465
65- const handleStart = ( clientX , clientY ) => {
6666 const rect = imageRef . current . getBoundingClientRect ( ) ;
67- const x = clientX - rect . left ;
68- const y = clientY - rect . top ;
69-
70- if ( smartModeActive ) {
71- // Send normalized smart click
72- const normX = x / rect . width ;
73- const normY = y / rect . height ;
74- sendSmartClick ( normX , normY ) ;
75- return ;
76- }
67+ const x = Math . round ( e . clientX - rect . left ) ;
68+ const y = Math . round ( e . clientY - rect . top ) ;
7769
7870 setStartPos ( { x, y } ) ;
7971 setCurrentPos ( { x, y } ) ;
8072 setBoundingBox ( null ) ;
81- } ;
73+ } , [ smartModeActive ] ) ;
74+
75+ const handlePointerMove = useCallback ( ( e ) => {
76+ if ( ! draggingRef . current || smartModeActive ) return ;
77+ if ( ! imageRef . current ) return ;
78+
79+ const rect = imageRef . current . getBoundingClientRect ( ) ;
80+ // Clamp to container bounds for clean drawing
81+ const x = Math . round ( Math . max ( 0 , Math . min ( e . clientX - rect . left , rect . width ) ) ) ;
82+ const y = Math . round ( Math . max ( 0 , Math . min ( e . clientY - rect . top , rect . height ) ) ) ;
83+
84+ setCurrentPos ( { x, y } ) ;
85+ } , [ smartModeActive ] ) ;
86+
87+ const handlePointerUp = useCallback ( async ( e ) => {
88+ if ( ! draggingRef . current ) return ;
89+ draggingRef . current = false ;
8290
83- const handleMove = ( clientX , clientY ) => {
84- if ( startPos && ! smartModeActive ) {
85- const rect = imageRef . current . getBoundingClientRect ( ) ;
86- const x = clientX - rect . left ;
87- const y = clientY - rect . top ;
88- setCurrentPos ( { x, y } ) ;
91+ if ( e . target . hasPointerCapture ( e . pointerId ) ) {
92+ e . target . releasePointerCapture ( e . pointerId ) ;
8993 }
90- } ;
9194
92- const handleEnd = async ( ) => {
93- if ( ! startPos || ! currentPos || smartModeActive ) return ;
95+ // Read refs for the final computation
96+ const start = startPos ;
97+ const current = currentPos ;
98+ if ( ! start || ! current || smartModeActive ) {
99+ setStartPos ( null ) ;
100+ setCurrentPos ( null ) ;
101+ return ;
102+ }
94103
95104 const rect = imageRef . current . getBoundingClientRect ( ) ;
96- const distance = getDistance ( startPos , currentPos ) ;
105+ const distance = getDistance ( start , current ) ;
97106 let bbox ;
98107
99108 const dragThreshold = Math . max ( 5 , ( window . devicePixelRatio || 1 ) * 5 ) ;
100109 if ( distance < dragThreshold ) {
101- const centerX = startPos . x ;
102- const centerY = startPos . y ;
110+ // Click-to-center: create default-size box around click point
111+ const centerX = start . x ;
112+ const centerY = start . y ;
103113 const width = rect . width * defaultBoundingBoxSize ;
104114 const height = rect . height * defaultBoundingBoxSize ;
105115 const left = centerX - width / 2 ;
@@ -112,12 +122,18 @@ const useBoundingBoxHandlers = (isTracking, setIsTracking, smartModeActive = fal
112122 height : defaultBoundingBoxSize ,
113123 } ;
114124
115- setBoundingBox ( { left, top, width, height } ) ;
125+ setBoundingBox ( {
126+ left : Math . round ( left ) ,
127+ top : Math . round ( top ) ,
128+ width : Math . round ( width ) ,
129+ height : Math . round ( height ) ,
130+ } ) ;
116131 } else {
117- const x1 = startPos . x / rect . width ;
118- const y1 = startPos . y / rect . height ;
119- const x2 = currentPos . x / rect . width ;
120- const y2 = currentPos . y / rect . height ;
132+ // Drag: compute normalized bbox
133+ const x1 = start . x / rect . width ;
134+ const y1 = start . y / rect . height ;
135+ const x2 = current . x / rect . width ;
136+ const y2 = current . y / rect . height ;
121137
122138 bbox = {
123139 x : Math . min ( x1 , x2 ) ,
@@ -127,10 +143,10 @@ const useBoundingBoxHandlers = (isTracking, setIsTracking, smartModeActive = fal
127143 } ;
128144
129145 setBoundingBox ( {
130- left : Math . min ( startPos . x , currentPos . x ) ,
131- top : Math . min ( startPos . y , currentPos . y ) ,
132- width : Math . abs ( currentPos . x - startPos . x ) ,
133- height : Math . abs ( currentPos . y - startPos . y ) ,
146+ left : Math . round ( Math . min ( start . x , current . x ) ) ,
147+ top : Math . round ( Math . min ( start . y , current . y ) ) ,
148+ width : Math . round ( Math . abs ( current . x - start . x ) ) ,
149+ height : Math . round ( Math . abs ( current . y - start . y ) ) ,
134150 } ) ;
135151 }
136152
@@ -143,38 +159,16 @@ const useBoundingBoxHandlers = (isTracking, setIsTracking, smartModeActive = fal
143159
144160 setStartPos ( null ) ;
145161 setCurrentPos ( null ) ;
146- } ;
147-
148- const handleMouseDown = ( e ) => handleStart ( e . clientX , e . clientY ) ;
149- const handleMouseMove = ( e ) => handleMove ( e . clientX , e . clientY ) ;
150- const handleMouseUp = handleEnd ;
151-
152- const handleTouchStart = ( e ) => {
153- e . preventDefault ( ) ;
154- handleStart ( e . touches [ 0 ] . clientX , e . touches [ 0 ] . clientY ) ;
155- } ;
156-
157- const handleTouchMove = ( e ) => {
158- e . preventDefault ( ) ;
159- handleMove ( e . touches [ 0 ] . clientX , e . touches [ 0 ] . clientY ) ;
160- } ;
161-
162- const handleTouchEnd = ( e ) => {
163- e . preventDefault ( ) ;
164- handleEnd ( ) ;
165- } ;
162+ } , [ startPos , currentPos , smartModeActive , defaultBoundingBoxSize , startTracking ] ) ;
166163
167164 return {
168165 imageRef,
169166 startPos,
170167 currentPos,
171168 boundingBox,
172- handleMouseDown,
173- handleMouseMove,
174- handleMouseUp,
175- handleTouchStart,
176- handleTouchMove,
177- handleTouchEnd,
169+ handlePointerDown,
170+ handlePointerMove,
171+ handlePointerUp,
178172 } ;
179173} ;
180174
0 commit comments