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/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>
<script type="module" crossorigin src="/assets/index-aRmVid5V.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BU1LuRPM.css">
<script type="module" crossorigin src="/assets/index-CYuyI8Zg.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CoyqmROJ.css">
</head>
<body>
<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.
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import CardFrame from '../CardFrame/index.jsx';
import CameraTiltControl from '../CameraTiltControl/index.jsx';
import GPIOToggleControl from '../GPIOToggleControl/index.jsx';
import ReplaySourcesPanel from '../ReplaySourcesPanel/index.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_AUDIO_RETRY_MS = 1000;
const PTZ_ZOOM_SLIDER_MIN = -1;
const PTZ_ZOOM_SLIDER_MAX = 1;
const PTZ_ZOOM_SPEED = 0.55;
function formatRemaining(deadline) {
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 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(() => {
ptzStop().catch(() => {});
}, [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 (
<CardFrame title="Zoom" bodyClassName="p-1 text-sm">
<CameraTiltControl
value={0}
min={PTZ_ZOOM_SLIDER_MIN}
max={PTZ_ZOOM_SLIDER_MAX}
step={1}
label="Zoom"
<CardFrame title="Zoom" className="md:hidden" bodyClassName="grid grid-cols-2 gap-0.5 p-1 text-sm">
<button
type="button"
className="mobile-touch-control button-dark min-h-12 text-xs disabled:opacity-50"
disabled={disabled}
onChange={sendZoom}
onCommit={stopZoom}
className="rounded-xl border-2 border-emerald-300/70 bg-emerald-900 px-1 py-1 text-emerald-50"
labelRowClass="text-xs text-emerald-100"
labelClass="text-sm font-semibold"
valueClass="hidden"
sliderClass="w-full"
accentClass="accent-emerald-400"
endpointClass="text-[0.7rem] text-emerald-100/80"
endpointLabelClass="w-14"
/>
<div className="mt-0.5 flex justify-between text-[0.7rem] text-slate-400">
<span>out</span>
<span>release stops</span>
<span>in</span>
onPointerDown={startZoom(-1)}
onPointerUp={stopFromPointer}
onPointerCancel={stopFromPointer}
onPointerLeave={stopFromPointer}
onContextMenu={(event) => event.preventDefault()}
>
Zoom out
</button>
<button
type="button"
className="mobile-touch-control button-dark min-h-12 text-xs disabled:opacity-50"
disabled={disabled}
onPointerDown={startZoom(1)}
onPointerUp={stopFromPointer}
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>
</CardFrame>
);
}
function keyLabelFor(keymap, actionId) {
return formatKeyLabel(keymap?.[actionId]?.[0]);
}
function PtzControlReference() {
const keymap = useControlSelector((control) => control.state.keymap);
const rows = [
['Pan / tilt', 'Drive movement'],
['Zoom in', formatKeyLabel(keymap?.cameraUp?.[0]) || 'camera up'],
['Zoom out', formatKeyLabel(keymap?.cameraDown?.[0]) || 'camera down'],
['Spotlight', formatKeyLabel(keymap?.headlightToggle?.[0]) || 'headlight'],
['Infrared mode', formatKeyLabel(keymap?.laserToggle?.[0]) || 'laser'],
['Tilt up', keyLabelFor(keymap, 'driveForward')],
['Tilt down', keyLabelFor(keymap, 'driveBackward')],
['Pan left', keyLabelFor(keymap, 'driveLeft')],
['Pan right', keyLabelFor(keymap, 'driveRight')],
['Zoom in', keyLabelFor(keymap, 'cameraUp')],
['Zoom out', keyLabelFor(keymap, 'cameraDown')],
['Spotlight', keyLabelFor(keymap, 'headlightToggle')],
['Infrared mode', keyLabelFor(keymap, 'laserToggle')],
];
return (
@@ -410,7 +428,7 @@ function PtzController({ open, onClose }) {
<PtzLightingControls ptz={ptz} />
</div>
<div className="shrink-0">
<PtzZoomControl />
<PtzMobileZoomButtons />
</div>
</>
) : (