Address review feedback: drag edge cases and test clarity

- Add onMouseLeave handler to cancel drag when cursor leaves overlay
- Preserve autotracking zooming disable-on-failure during init
- Extract 20px drag threshold as MIN_DRAG_DISTANCE constant
- Add sync-with-source notes to replicated test helpers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ryan Gregg 2026-03-18 02:18:44 +00:00
parent 4dfc248df4
commit 4a00dbca9d
3 changed files with 26 additions and 6 deletions

View File

@ -295,6 +295,13 @@ class OnvifController:
): ):
del move_request["Speed"]["Zoom"] del move_request["Speed"]["Zoom"]
except Exception as e: except Exception as e:
if (
self.config.cameras[camera_name].onvif.autotracking.zooming
== ZoomingModeEnum.relative
):
self.config.cameras[
camera_name
].onvif.autotracking.zooming = ZoomingModeEnum.disabled
logger.warning( logger.warning(
f"Relative zoom not supported for {camera_name}: {e}" f"Relative zoom not supported for {camera_name}: {e}"
) )

View File

@ -20,7 +20,12 @@ class OnvifCommandEnum(str, Enum):
class TestMoveRelativeParsing(TestCase): class TestMoveRelativeParsing(TestCase):
"""Test the move_relative command parameter parsing logic.""" """Test the move_relative command parameter parsing logic.
NOTE: _parse_move_relative replicates logic from
OnvifController.handle_command_async (frigate/ptz/onvif.py).
If that parsing changes, this helper must be updated to match.
"""
def _parse_move_relative(self, param: str): def _parse_move_relative(self, param: str):
"""Replicate the parsing logic from OnvifController.handle_command_async.""" """Replicate the parsing logic from OnvifController.handle_command_async."""
@ -57,8 +62,10 @@ class TestMoveRelativeParsing(TestCase):
class TestDispatcherPtzParsing(TestCase): class TestDispatcherPtzParsing(TestCase):
"""Test the dispatcher's _on_ptz_command parsing pipeline. """Test the dispatcher's _on_ptz_command parsing pipeline.
Replicates the logic from FrigateDispatcher._on_ptz_command to verify Replicates the logic from FrigateDispatcher._on_ptz_command
that MQTT payloads are correctly decomposed into command + param. (frigate/comms/dispatcher.py) to verify that MQTT payloads are
correctly decomposed into command + param.
If that parsing changes, this helper must be updated to match.
""" """
def _parse_ptz_payload(self, payload: str): def _parse_ptz_payload(self, payload: str):

View File

@ -131,6 +131,9 @@ type LiveCameraViewProps = {
toggleFullscreen: () => void; toggleFullscreen: () => void;
}; };
/** Minimum drag distance (px) to distinguish a drag-to-zoom from a click-to-move. */
const MIN_DRAG_DISTANCE = 20;
function getClientPos( function getClientPos(
e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>, e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>,
): { x: number; y: number } | null { ): { x: number; y: number } | null {
@ -293,8 +296,7 @@ export default function LiveCameraView({
const dx = Math.abs(pos.x - dragStart.x); const dx = Math.abs(pos.x - dragStart.x);
const dy = Math.abs(pos.y - dragStart.y); const dy = Math.abs(pos.y - dragStart.y);
// Minimum drag distance of 20px to distinguish from click if (dx < MIN_DRAG_DISTANCE && dy < MIN_DRAG_DISTANCE) {
if (dx < 20 && dy < 20) {
// Click (not drag) — move to point without zoom // Click (not drag) — move to point without zoom
const normalizedX = (pos.x - rect.left) / rect.width; const normalizedX = (pos.x - rect.left) / rect.width;
const normalizedY = (pos.y - rect.top) / rect.height; const normalizedY = (pos.y - rect.top) / rect.height;
@ -352,7 +354,7 @@ export default function LiveCameraView({
if (!isDragging || !clickOverlayRef.current) return null; if (!isDragging || !clickOverlayRef.current) return null;
const dx = Math.abs(dragCurrent.x - dragStart.x); const dx = Math.abs(dragCurrent.x - dragStart.x);
const dy = Math.abs(dragCurrent.y - dragStart.y); const dy = Math.abs(dragCurrent.y - dragStart.y);
if (dx < 20 && dy < 20) return null; // Don't show rectangle for small movements if (dx < MIN_DRAG_DISTANCE && dy < MIN_DRAG_DISTANCE) return null;
const rect = clickOverlayRef.current.getBoundingClientRect(); const rect = clickOverlayRef.current.getBoundingClientRect();
const x1 = Math.min(dragStart.x, dragCurrent.x) - rect.left; const x1 = Math.min(dragStart.x, dragCurrent.x) - rect.left;
@ -762,6 +764,10 @@ export default function LiveCameraView({
onMouseDown={handleOverlayMouseDown} onMouseDown={handleOverlayMouseDown}
onMouseMove={handleOverlayMouseMove} onMouseMove={handleOverlayMouseMove}
onMouseUp={handleOverlayMouseUp} onMouseUp={handleOverlayMouseUp}
onMouseLeave={() => {
setDragStart(null);
setDragCurrent(null);
}}
onTouchStart={handleOverlayMouseDown} onTouchStart={handleOverlayMouseDown}
onTouchMove={handleOverlayMouseMove} onTouchMove={handleOverlayMouseMove}
onTouchEnd={handleOverlayMouseUp} onTouchEnd={handleOverlayMouseUp}