Skip to content

Commit bad344b

Browse files
alireza787bclaude
andcommitted
fix(dashboard): rewrite drag-to-track with Pointer Events and pointer capture
Classic mode bounding box drawing was unreliable — the rectangle stopped tracking when the cursor left the video container, coordinates were sub-pixel (blurry), and the overlay had no z-index (could render behind the video canvas). Changes: - useBoundingBoxHandlers: Replace separate mouse + touch handlers with unified Pointer Events. Use setPointerCapture() so drag continues even when pointer leaves the container. Clamp coordinates to container bounds. Round to integer pixels for crisp rendering. - BoundingBoxDrawer: Wire onPointerDown/Move/Up instead of 6 separate mouse/touch handlers. Add z-index: 5 to bbox overlay. Add userSelect: none to prevent text selection during drag. Use 2px border with subtle fill for cleaner visual. - DashboardPage: Update destructured props to match new API. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4ad9fb2 commit bad344b

File tree

3 files changed

+102
-116
lines changed

3 files changed

+102
-116
lines changed

dashboard/src/components/BoundingBoxDrawer.js

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,9 @@ const BoundingBoxDrawer = ({
2323
startPos,
2424
currentPos,
2525
boundingBox,
26-
handleMouseDown,
27-
handleMouseMove,
28-
handleMouseUp,
29-
handleTouchStart,
30-
handleTouchMove,
31-
handleTouchEnd,
26+
handlePointerDown,
27+
handlePointerMove,
28+
handlePointerUp,
3229
videoSrc,
3330
protocol,
3431
smartModeActive,
@@ -68,13 +65,13 @@ const BoundingBoxDrawer = ({
6865

6966
const isDrawing = (startPos && currentPos) || boundingBox;
7067

71-
let left, top, width, height;
68+
let left = 0, top = 0, width = 0, height = 0;
7269

7370
if (isDrawing && containerDimensions.width && containerDimensions.height) {
74-
left = boundingBox ? boundingBox.left : Math.min(startPos.x, currentPos.x);
75-
top = boundingBox ? boundingBox.top : Math.min(startPos.y, currentPos.y);
76-
width = boundingBox ? boundingBox.width : Math.abs(currentPos.x - startPos.x);
77-
height = boundingBox ? boundingBox.height : Math.abs(currentPos.y - startPos.y);
71+
left = boundingBox ? boundingBox.left : Math.round(Math.min(startPos.x, currentPos.x));
72+
top = boundingBox ? boundingBox.top : Math.round(Math.min(startPos.y, currentPos.y));
73+
width = boundingBox ? boundingBox.width : Math.round(Math.abs(currentPos.x - startPos.x));
74+
height = boundingBox ? boundingBox.height : Math.round(Math.abs(currentPos.y - startPos.y));
7875

7976
const containerWidth = containerDimensions.width;
8077
const containerHeight = containerDimensions.height;
@@ -113,15 +110,14 @@ const BoundingBoxDrawer = ({
113110
display: 'block',
114111
width: '100%',
115112
touchAction: 'none',
113+
userSelect: 'none',
114+
WebkitUserSelect: 'none',
116115
cursor: smartModeActive ? 'crosshair' : (startPos ? 'crosshair' : 'cell'),
117116
}}
118-
onMouseDown={!smartModeActive ? handleMouseDown : null}
119-
onMouseMove={!smartModeActive ? handleMouseMove : null}
120-
onMouseUp={!smartModeActive ? handleMouseUp : null}
121-
onTouchStart={!smartModeActive ? handleTouchStart : null}
122-
onTouchMove={!smartModeActive ? handleTouchMove : null}
123-
onTouchEnd={!smartModeActive ? handleTouchEnd : null}
124-
onClick={smartModeActive ? handleSmartClick : null}
117+
onPointerDown={!smartModeActive ? handlePointerDown : undefined}
118+
onPointerMove={!smartModeActive ? handlePointerMove : undefined}
119+
onPointerUp={!smartModeActive ? handlePointerUp : undefined}
120+
onClick={smartModeActive ? handleSmartClick : undefined}
125121
>
126122
<VideoStream protocol={protocol} src={videoSrc} />
127123

@@ -180,14 +176,16 @@ const BoundingBoxDrawer = ({
180176
<div
181177
style={{
182178
position: 'absolute',
183-
border: '3px solid #ff5722',
184-
borderRadius: '2px',
185-
left: left,
186-
top: top,
187-
width: width,
188-
height: height,
179+
left,
180+
top,
181+
width,
182+
height,
183+
border: '2px solid #ff5722',
184+
borderRadius: 1,
185+
backgroundColor: 'rgba(255, 87, 34, 0.08)',
189186
pointerEvents: 'none',
190-
boxShadow: '0 0 0 2px rgba(255, 87, 34, 0.2), 0 0 8px rgba(255, 87, 34, 0.4)',
187+
zIndex: 5,
188+
boxShadow: '0 0 0 1px rgba(255, 87, 34, 0.3)',
191189
}}
192190
/>
193191
)}

dashboard/src/hooks/useBoundingBoxHandlers.js

Lines changed: 73 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,27 @@
1-
import { useState, useRef, useEffect } from 'react';
1+
import { useState, useRef, useEffect, useCallback } from 'react';
22
import { 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+
*/
410
const 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

dashboard/src/pages/DashboardPage.js

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,9 @@ const DashboardPage = () => {
8888
startPos,
8989
currentPos,
9090
boundingBox,
91-
handleMouseDown,
92-
handleMouseMove,
93-
handleMouseUp,
94-
handleTouchStart,
95-
handleTouchMove,
96-
handleTouchEnd,
91+
handlePointerDown,
92+
handlePointerMove,
93+
handlePointerUp,
9794
} = useBoundingBoxHandlers(isTracking, setIsTracking, smartModeActive);
9895

9996
const handleTrackingToggle = async () => {
@@ -294,12 +291,9 @@ const DashboardPage = () => {
294291
startPos={startPos}
295292
currentPos={currentPos}
296293
boundingBox={boundingBox}
297-
handleMouseDown={handleMouseDown}
298-
handleMouseMove={handleMouseMove}
299-
handleMouseUp={handleMouseUp}
300-
handleTouchStart={handleTouchStart}
301-
handleTouchMove={handleTouchMove}
302-
handleTouchEnd={handleTouchEnd}
294+
handlePointerDown={handlePointerDown}
295+
handlePointerMove={handlePointerMove}
296+
handlePointerUp={handlePointerUp}
303297
videoSrc={videoFeed}
304298
protocol={streamingProtocol}
305299
smartModeActive={smartModeActive}

0 commit comments

Comments
 (0)