// Mobile Controls Content // Purpose: Defines the Mobile Controls Content module and the local helpers/components used in this file. // Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit. import { useCallback, useEffect, useRef } from 'react'; import { useControlSystem } from '../../controls/index.js'; import { clampUnit } from '../../controls/controlMath.js'; import DriveDockAction, { useDriveDockState } from '../DriveDockAction/index.jsx'; import NightVisionControl from '../NightVisionControl/index.jsx'; import HornControl from '../HornControl/index.jsx'; import CameraTiltControl from '../CameraTiltControl/index.jsx'; import FloatingJoystick from './FloatingJoystick.jsx'; import MobileAuxButton from './MobileAuxButton.jsx'; import { SOURCE, JOYSTICK_RADIUS, JOYSTICK_SMOOTHING, AUX_ZERO, AUX_ALL_FORWARD, AUX_ALL_BACKWARD, } from './constants.js'; import { useManualDockAssist } from '../../features/manualDockAssist/useManualDockAssist.js'; function MobileJoystickPanel({ layout }) { const { state: { roverId }, actions: { setDriveVector, registerInputState }, } = useControlSystem(); const driveDockState = useDriveDockState(roverId); const dockedNotDriving = driveDockState.docked && !driveDockState.driving; const expandAction = dockedNotDriving || driveDockState.dockingInProgress; const disabled = !roverId; const joystickRadius = JOYSTICK_RADIUS; const smoothing = JOYSTICK_SMOOTHING; const smoothedVectorRef = useRef({ x: 0, y: 0, boost: false }); useEffect(() => { if (disabled) smoothedVectorRef.current = { x: 0, y: 0, boost: false }; }, [disabled]); const handleMove = useCallback( (vector = {}) => { if (disabled) return; const next = { x: clampUnit(vector.x ?? 0), y: clampUnit(vector.y ?? 0), boost: Boolean(vector.boost), }; const applied = smoothing > 0 ? { x: smoothedVectorRef.current.x + (next.x - smoothedVectorRef.current.x) * (1 - smoothing), y: smoothedVectorRef.current.y + (next.y - smoothedVectorRef.current.y) * (1 - smoothing), boost: next.boost, } : next; smoothedVectorRef.current = applied; setDriveVector(applied, { source: SOURCE }); registerInputState(SOURCE, { vector: applied, lastEvent: 'move' }); }, [disabled, registerInputState, setDriveVector, smoothing], ); const handleStop = useCallback(() => { if (disabled) return; const zero = { x: 0, y: 0, boost: false }; smoothedVectorRef.current = zero; setDriveVector(zero, { source: SOURCE }); registerInputState(SOURCE, { vector: zero, lastEvent: 'stop' }); }, [disabled, registerInputState, setDriveVector]); const fillClass = dockedNotDriving ? 'max-h-screen self-start' : ''; const containerClass = `flex h-full flex-col gap-0.5 text-slate-100 ${fillClass}`; return (
{!expandAction ? (
) : null}
); } function MobileActionsColumnContent({ layout }) { const { state: { roverId, camera, horn }, pipeline, actions: { setServoAngle, setNightVision, setAuxMotors, startHorn, stopHorn }, } = useControlSystem(); const dockAssist = useManualDockAssist(); const disabled = !roverId; const activeRef = useRef(null); const cameraConfig = camera?.config; const cameraEnabled = Boolean(roverId && camera?.enabled && cameraConfig); const nightVisionAvailable = Boolean(roverId && pipeline?.nightVision); const nightVisionState = pipeline?.nightVisionState; const hornAvailable = Boolean(roverId && pipeline?.horn); const hornBlocked = horn?.overheated; const cameraMin = typeof cameraConfig?.minAngle === 'number' ? cameraConfig.minAngle : -45; const cameraMax = typeof cameraConfig?.maxAngle === 'number' ? cameraConfig.maxAngle : 45; const cameraValue = typeof camera?.angle === 'number' ? camera.angle : typeof cameraConfig?.homeAngle === 'number' ? cameraConfig.homeAngle : (cameraMin + cameraMax) / 2; const cameraDisabled = Boolean(disabled || dockAssist.cameraLocked); const handleNightVisionToggle = useCallback( (nextOn) => { if (!nightVisionAvailable) return; setNightVision(nextOn); }, [nightVisionAvailable, setNightVision], ); const handleAuxPress = useCallback( (id, values) => { if (disabled) return; activeRef.current = id; setAuxMotors(values); }, [disabled, setAuxMotors], ); const handleAuxRelease = useCallback( (id) => { if (disabled) return; if (activeRef.current === id) { activeRef.current = null; setAuxMotors(AUX_ZERO); } }, [disabled, setAuxMotors], ); return (
{cameraEnabled ? (
) : null} {nightVisionAvailable ? ( ) : null}
{hornAvailable ? ( ) : null}
); } export function MobileActionsColumn({ layout, className = '' }) { return (
); } export function MobileDriveColumn({ layout, className = '' }) { return (
); } export default function MobilePortraitControls({ swapColumns = false }) { const columnHeight = 'h-[min(60svh,24rem)]'; const firstColumn = swapColumns ? : ; const secondColumn = swapColumns ? : ; return (
{firstColumn} {secondColumn}
); }