mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 17:40:46 -04:00
ptz control updates
This commit is contained in:
@@ -148,13 +148,25 @@ export default function ControlPadPanel({ compact = false, disabled = false }) {
|
||||
return () => {
|
||||
clearRepeatTimer();
|
||||
/*
|
||||
Mobile controls can unmount when layouts change or the driver leaves the
|
||||
control surface. Clear the shared flag so a stale mobile precision choice
|
||||
cannot leave desktop/keyboard camera tilt in fine-step mode.
|
||||
Mobile controls can unmount during an orientation/layout change while a
|
||||
pointer is still captured by the disappearing element. Publish a neutral
|
||||
vector directly during cleanup so neither rover drive nor PTZ pan/tilt
|
||||
can retain the last cell merely because pointerup had nowhere to land.
|
||||
*/
|
||||
activeCellRef.current = null;
|
||||
setDriveVector({ x: 0, y: 0, boost: false }, { source: SOURCE });
|
||||
registerInputState(SOURCE, {
|
||||
keys: [],
|
||||
vector: { x: 0, y: 0, boost: false },
|
||||
activeCell: 'stop',
|
||||
speedMode: speedModeRef.current,
|
||||
lastEvent: 'unmount',
|
||||
});
|
||||
// Clear the shared flag too, so a stale mobile precision choice cannot
|
||||
// leave desktop/keyboard camera tilt in fine-step mode.
|
||||
setCameraPrecisionMode(false);
|
||||
};
|
||||
}, [clearRepeatTimer, setCameraPrecisionMode]);
|
||||
}, [clearRepeatTimer, registerInputState, setCameraPrecisionMode, setDriveVector]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!disabled) return;
|
||||
|
||||
@@ -188,46 +188,30 @@ function PtzLightingControls({ ptz, disabled = false }) {
|
||||
}
|
||||
|
||||
function PtzMobileZoomButtons({ disabled = false }) {
|
||||
const { nudgeServo } = useControlActions();
|
||||
const repeatTimerRef = useRef(null);
|
||||
const { setCameraAxisIntent } = useControlActions();
|
||||
|
||||
const stopZoom = useCallback(() => {
|
||||
/*
|
||||
Mobile zoom is intentionally routed through the normal camera-up/down
|
||||
control action instead of emitting PTZ socket commands directly. That
|
||||
keeps the zoom buttons on the same path as keyboard/gamepad camera tilt,
|
||||
and the PTZ adapter remains the one place that translates "camera nudge"
|
||||
into Reolink zoom pulses.
|
||||
Zero only releases the zoom axis. The PTZ adapter combines it with any
|
||||
pan/tilt direction still held on the movement pad, so lifting one finger
|
||||
cannot erase the other finger's intent.
|
||||
*/
|
||||
if (repeatTimerRef.current) {
|
||||
clearInterval(repeatTimerRef.current);
|
||||
repeatTimerRef.current = null;
|
||||
}
|
||||
/*
|
||||
Zero is a zoom-only release signal in the PTZ adapter. Using the global
|
||||
stop action here previously erased a simultaneously held pan/tilt vector,
|
||||
making mixed touch controls unexpectedly stop the camera.
|
||||
*/
|
||||
nudgeServo(0);
|
||||
}, [nudgeServo]);
|
||||
setCameraAxisIntent(0);
|
||||
}, [setCameraAxisIntent]);
|
||||
|
||||
const startZoom = useCallback(
|
||||
(direction) => (event) => {
|
||||
/*
|
||||
Send an immediate nudge and then repeat while held. The adapter turns
|
||||
each nudge into a short zoom pulse, so repeating the standard action is
|
||||
the simplest way to get continuous hold-to-zoom without adding another
|
||||
PTZ-specific command loop.
|
||||
Publish held state once. The adapter owns the single motion heartbeat,
|
||||
so this button no longer creates a second interval whose queued callback
|
||||
could run after pointerup and restart zoom.
|
||||
*/
|
||||
event.preventDefault();
|
||||
if (disabled) return;
|
||||
stopZoom();
|
||||
nudgeServo(direction);
|
||||
repeatTimerRef.current = setInterval(() => {
|
||||
nudgeServo(direction);
|
||||
}, 120);
|
||||
event.currentTarget.setPointerCapture?.(event.pointerId);
|
||||
setCameraAxisIntent(direction);
|
||||
},
|
||||
[disabled, nudgeServo, stopZoom],
|
||||
[disabled, setCameraAxisIntent],
|
||||
);
|
||||
const stopFromPointer = useCallback(
|
||||
(event) => {
|
||||
@@ -242,16 +226,12 @@ function PtzMobileZoomButtons({ disabled = false }) {
|
||||
() => () => {
|
||||
/*
|
||||
A touch surface can unmount during orientation changes or fullscreen
|
||||
close while a pointer is still down. Clear the repeat timer here so a
|
||||
held zoom button cannot keep firing camera-up/down actions after the
|
||||
mobile controls have disappeared.
|
||||
close while a pointer is still down. Explicitly clear zoom here because
|
||||
an unmounted DOM node cannot deliver its pointerup/pointercancel event.
|
||||
*/
|
||||
if (repeatTimerRef.current) {
|
||||
clearInterval(repeatTimerRef.current);
|
||||
repeatTimerRef.current = null;
|
||||
}
|
||||
setCameraAxisIntent(0);
|
||||
},
|
||||
[],
|
||||
[setCameraAxisIntent],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -263,7 +243,7 @@ function PtzMobileZoomButtons({ disabled = false }) {
|
||||
onPointerDown={startZoom(-1)}
|
||||
onPointerUp={stopFromPointer}
|
||||
onPointerCancel={stopFromPointer}
|
||||
onPointerLeave={stopFromPointer}
|
||||
onLostPointerCapture={stopFromPointer}
|
||||
onContextMenu={(event) => event.preventDefault()}
|
||||
>
|
||||
Zoom out
|
||||
@@ -275,7 +255,7 @@ function PtzMobileZoomButtons({ disabled = false }) {
|
||||
onPointerDown={startZoom(1)}
|
||||
onPointerUp={stopFromPointer}
|
||||
onPointerCancel={stopFromPointer}
|
||||
onPointerLeave={stopFromPointer}
|
||||
onLostPointerCapture={stopFromPointer}
|
||||
onContextMenu={(event) => event.preventDefault()}
|
||||
>
|
||||
Zoom in
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Vip PTZ Camera Card
|
||||
// Purpose: Provides the verified-user entry point and fullscreen controller for the single Reolink PTZ camera.
|
||||
// Scope: Owns PTZ UI state only; server-side PTZ ownership, rover handoff, and command authorization remain authoritative.
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import ChatPanel from '../ChatPanel/index.jsx';
|
||||
import CardFrame from '../CardFrame/index.jsx';
|
||||
@@ -11,13 +11,12 @@ import PtzLiveVideo from '../PtzLiveVideo/index.jsx';
|
||||
import ReplaySourcesPanel from '../ReplaySourcesPanel/index.jsx';
|
||||
import KeyPill from './VipAudioUploadCard/KeyPill.jsx';
|
||||
import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx';
|
||||
import { useControlSelector } from '../../controls/index.js';
|
||||
import { useControlActions, useControlSelector } from '../../controls/index.js';
|
||||
import { formatKeyLabel } from '../../controls/keymapUtils.js';
|
||||
import { usePtzCameraSnapshots } from '../../hooks/usePtzCameraSnapshot.js';
|
||||
import { isFeatureEnabled } from '../../lib/features.js';
|
||||
|
||||
const PTZ_CAMERA_ID = 'ptz-camera';
|
||||
const PTZ_ZOOM_SPEED = 0.55;
|
||||
|
||||
function formatRemaining(deadline) {
|
||||
const remaining = Math.max(0, Math.ceil((Number(deadline || 0) - Date.now()) / 1000));
|
||||
@@ -218,22 +217,25 @@ function PtzLightingControls({ ptz, disabled = false }) {
|
||||
}
|
||||
|
||||
function PtzMobileZoomButtons({ disabled = false }) {
|
||||
const { ptzMove, ptzStop } = useSessionActions();
|
||||
const { setCameraAxisIntent } = useControlActions();
|
||||
const stopZoom = useCallback(() => {
|
||||
ptzStop().catch(() => {});
|
||||
}, [ptzStop]);
|
||||
// This is a zoom-only release; the shared adapter retains any simultaneous
|
||||
// pan/tilt intent from the movement pad in its next combined motion state.
|
||||
setCameraAxisIntent(0);
|
||||
}, [setCameraAxisIntent]);
|
||||
const startZoom = useCallback(
|
||||
(direction) => (event) => {
|
||||
/*
|
||||
Mobile needs explicit zoom targets because the regular mobile drive pad
|
||||
is already used for pan/tilt. Desktop does not render these buttons; it
|
||||
uses the mapped camera up/down controls shown in the reference panel.
|
||||
The adapter owns renewal for held PTZ state. Publishing the direction
|
||||
once avoids a component-local repeat timer and keeps this legacy card on
|
||||
the exact same motion path as the dedicated PTZ route.
|
||||
*/
|
||||
event.preventDefault();
|
||||
if (disabled) return;
|
||||
ptzMove({ pan: 0, tilt: 0, zoom: direction * PTZ_ZOOM_SPEED }).catch(() => {});
|
||||
event.currentTarget.setPointerCapture?.(event.pointerId);
|
||||
setCameraAxisIntent(direction);
|
||||
},
|
||||
[disabled, ptzMove],
|
||||
[disabled, setCameraAxisIntent],
|
||||
);
|
||||
const stopFromPointer = useCallback(
|
||||
(event) => {
|
||||
@@ -244,6 +246,15 @@ function PtzMobileZoomButtons({ disabled = false }) {
|
||||
[disabled, stopZoom],
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
// Orientation changes can unmount the button before pointerup. Clear the
|
||||
// held zoom state explicitly instead of waiting for the server watchdog.
|
||||
setCameraAxisIntent(0);
|
||||
},
|
||||
[setCameraAxisIntent],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mobile-touch-control grid grid-cols-2 gap-0.5 text-sm">
|
||||
<button
|
||||
@@ -253,7 +264,7 @@ function PtzMobileZoomButtons({ disabled = false }) {
|
||||
onPointerDown={startZoom(-1)}
|
||||
onPointerUp={stopFromPointer}
|
||||
onPointerCancel={stopFromPointer}
|
||||
onPointerLeave={stopFromPointer}
|
||||
onLostPointerCapture={stopFromPointer}
|
||||
onContextMenu={(event) => event.preventDefault()}
|
||||
>
|
||||
Zoom out
|
||||
@@ -265,7 +276,7 @@ function PtzMobileZoomButtons({ disabled = false }) {
|
||||
onPointerDown={startZoom(1)}
|
||||
onPointerUp={stopFromPointer}
|
||||
onPointerCancel={stopFromPointer}
|
||||
onPointerLeave={stopFromPointer}
|
||||
onLostPointerCapture={stopFromPointer}
|
||||
onContextMenu={(event) => event.preventDefault()}
|
||||
>
|
||||
Zoom in
|
||||
|
||||
Reference in New Issue
Block a user