This commit is contained in:
legop3
2026-07-11 13:32:15 -04:00
parent 91bbeb6d8a
commit 026e9de476
6 changed files with 76 additions and 58 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -78,8 +78,8 @@
<script defer src="https://analytics.otter.land/script.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land"></script> <script defer src="https://analytics.otter.land/script.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land"></script>
<script defer src="https://analytics.otter.land/recorder.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land" data-sample-rate="0.15" data-mask-level="moderate" data-max-duration="300000"></script> <script defer src="https://analytics.otter.land/recorder.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land" data-sample-rate="0.15" data-mask-level="moderate" data-max-duration="300000"></script>
<title>Roomba Rover</title> <title>Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-aRmVid5V.js"></script> <script type="module" crossorigin src="/assets/index-CYuyI8Zg.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BU1LuRPM.css"> <link rel="stylesheet" crossorigin href="/assets/index-CoyqmROJ.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+62 -44
View File
@@ -3,7 +3,6 @@
// Scope: Owns PTZ UI state only; server-side PTZ ownership, rover handoff, and command authorization remain authoritative. // Scope: Owns PTZ UI state only; server-side PTZ ownership, rover handoff, and command authorization remain authoritative.
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import CardFrame from '../CardFrame/index.jsx'; import CardFrame from '../CardFrame/index.jsx';
import CameraTiltControl from '../CameraTiltControl/index.jsx';
import GPIOToggleControl from '../GPIOToggleControl/index.jsx'; import GPIOToggleControl from '../GPIOToggleControl/index.jsx';
import ReplaySourcesPanel from '../ReplaySourcesPanel/index.jsx'; import ReplaySourcesPanel from '../ReplaySourcesPanel/index.jsx';
import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx'; import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx';
@@ -16,8 +15,7 @@ import { isFeatureEnabled } from '../../lib/features.js';
const PTZ_CAMERA_ID = 'ptz-camera'; const PTZ_CAMERA_ID = 'ptz-camera';
const PTZ_AUDIO_RETRY_MS = 1000; const PTZ_AUDIO_RETRY_MS = 1000;
const PTZ_ZOOM_SLIDER_MIN = -1; const PTZ_ZOOM_SPEED = 0.55;
const PTZ_ZOOM_SLIDER_MAX = 1;
function formatRemaining(deadline) { function formatRemaining(deadline) {
const remaining = Math.max(0, Math.ceil((Number(deadline || 0) - Date.now()) / 1000)); const remaining = Math.max(0, Math.ceil((Number(deadline || 0) - Date.now()) / 1000));
@@ -291,61 +289,81 @@ function PtzLightingControls({ ptz, disabled = false }) {
); );
} }
function PtzZoomControl({ disabled = false }) { function PtzMobileZoomButtons({ disabled = false }) {
const { ptzMove, ptzStop } = useSessionActions(); const { ptzMove, ptzStop } = useSessionActions();
const sendZoom = useCallback(
(value) => {
if (disabled) return;
const zoom = Math.max(-1, Math.min(1, Number(value) || 0));
if (!zoom) {
ptzStop().catch(() => {});
return;
}
ptzMove({ pan: 0, tilt: 0, zoom }).catch(() => {});
},
[disabled, ptzMove, ptzStop],
);
const stopZoom = useCallback(() => { const stopZoom = useCallback(() => {
ptzStop().catch(() => {}); ptzStop().catch(() => {});
}, [ptzStop]); }, [ptzStop]);
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.
*/
event.preventDefault();
if (disabled) return;
ptzMove({ pan: 0, tilt: 0, zoom: direction * PTZ_ZOOM_SPEED }).catch(() => {});
},
[disabled, ptzMove],
);
const stopFromPointer = useCallback(
(event) => {
event?.preventDefault?.();
if (disabled) return;
stopZoom();
},
[disabled, stopZoom],
);
return ( return (
<CardFrame title="Zoom" bodyClassName="p-1 text-sm"> <CardFrame title="Zoom" className="md:hidden" bodyClassName="grid grid-cols-2 gap-0.5 p-1 text-sm">
<CameraTiltControl <button
value={0} type="button"
min={PTZ_ZOOM_SLIDER_MIN} className="mobile-touch-control button-dark min-h-12 text-xs disabled:opacity-50"
max={PTZ_ZOOM_SLIDER_MAX}
step={1}
label="Zoom"
disabled={disabled} disabled={disabled}
onChange={sendZoom} onPointerDown={startZoom(-1)}
onCommit={stopZoom} onPointerUp={stopFromPointer}
className="rounded-xl border-2 border-emerald-300/70 bg-emerald-900 px-1 py-1 text-emerald-50" onPointerCancel={stopFromPointer}
labelRowClass="text-xs text-emerald-100" onPointerLeave={stopFromPointer}
labelClass="text-sm font-semibold" onContextMenu={(event) => event.preventDefault()}
valueClass="hidden" >
sliderClass="w-full" Zoom out
accentClass="accent-emerald-400" </button>
endpointClass="text-[0.7rem] text-emerald-100/80" <button
endpointLabelClass="w-14" type="button"
/> className="mobile-touch-control button-dark min-h-12 text-xs disabled:opacity-50"
<div className="mt-0.5 flex justify-between text-[0.7rem] text-slate-400"> disabled={disabled}
<span>out</span> onPointerDown={startZoom(1)}
<span>release stops</span> onPointerUp={stopFromPointer}
<span>in</span> onPointerCancel={stopFromPointer}
onPointerLeave={stopFromPointer}
onContextMenu={(event) => event.preventDefault()}
>
Zoom in
</button>
<div className="col-span-2 text-center text-[0.7rem] text-slate-400">
Hold to zoom, release to stop
</div> </div>
</CardFrame> </CardFrame>
); );
} }
function keyLabelFor(keymap, actionId) {
return formatKeyLabel(keymap?.[actionId]?.[0]);
}
function PtzControlReference() { function PtzControlReference() {
const keymap = useControlSelector((control) => control.state.keymap); const keymap = useControlSelector((control) => control.state.keymap);
const rows = [ const rows = [
['Pan / tilt', 'Drive movement'], ['Tilt up', keyLabelFor(keymap, 'driveForward')],
['Zoom in', formatKeyLabel(keymap?.cameraUp?.[0]) || 'camera up'], ['Tilt down', keyLabelFor(keymap, 'driveBackward')],
['Zoom out', formatKeyLabel(keymap?.cameraDown?.[0]) || 'camera down'], ['Pan left', keyLabelFor(keymap, 'driveLeft')],
['Spotlight', formatKeyLabel(keymap?.headlightToggle?.[0]) || 'headlight'], ['Pan right', keyLabelFor(keymap, 'driveRight')],
['Infrared mode', formatKeyLabel(keymap?.laserToggle?.[0]) || 'laser'], ['Zoom in', keyLabelFor(keymap, 'cameraUp')],
['Zoom out', keyLabelFor(keymap, 'cameraDown')],
['Spotlight', keyLabelFor(keymap, 'headlightToggle')],
['Infrared mode', keyLabelFor(keymap, 'laserToggle')],
]; ];
return ( return (
@@ -410,7 +428,7 @@ function PtzController({ open, onClose }) {
<PtzLightingControls ptz={ptz} /> <PtzLightingControls ptz={ptz} />
</div> </div>
<div className="shrink-0"> <div className="shrink-0">
<PtzZoomControl /> <PtzMobileZoomButtons />
</div> </div>
</> </>
) : ( ) : (