mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
mobile reorganizations
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
// Control Pad Panel
|
||||
// Purpose: Provides the mobile movement control pad and speed mode selector.
|
||||
// Scope: Converts touch pad cells into keyboard-style drive vectors; movement column owns drive/dock placement.
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useControlSystem } from '../../controls/index.js';
|
||||
import { normalizeKeymapEntries } from '../../controls/keymapUtils.js';
|
||||
import {
|
||||
computeKeyboardDriveVector,
|
||||
getKeyboardDriveSpeedOptions,
|
||||
resolveKeyboardSpeeds,
|
||||
} from '../../controls/inputs/driveIntent.js';
|
||||
import { useSettingsNamespace } from '../../settings/index.js';
|
||||
import { INPUT_SETTINGS_DEFAULTS } from '../../settings/namespaces.js';
|
||||
import FloatingJoystick from './FloatingJoystick.jsx';
|
||||
import {
|
||||
DRIVE_PAD_REPEAT_MS,
|
||||
DRIVE_PAD_SPEED_MODES,
|
||||
SOURCE,
|
||||
} from './constants.js';
|
||||
|
||||
function firstTokenForAction(keymap, actionId) {
|
||||
const bindingSet = keymap?.[actionId];
|
||||
if (!bindingSet || bindingSet.size === 0) return null;
|
||||
return bindingSet.values().next().value ?? null;
|
||||
}
|
||||
|
||||
function getSpeedModeConfig(speedMode) {
|
||||
return DRIVE_PAD_SPEED_MODES.find((mode) => mode.id === speedMode) || DRIVE_PAD_SPEED_MODES[1];
|
||||
}
|
||||
|
||||
export default function ControlPadPanel({ disabled = false }) {
|
||||
const {
|
||||
state: { keymap: rawKeymap },
|
||||
actions: { setDriveVector, registerInputState },
|
||||
} = useControlSystem();
|
||||
const { value: inputSettings } = useSettingsNamespace('inputs', INPUT_SETTINGS_DEFAULTS);
|
||||
const [speedMode, setSpeedMode] = useState('normal');
|
||||
const [activeInputLabel, setActiveInputLabel] = useState('stop');
|
||||
const speedModeRef = useRef('normal');
|
||||
const activeCellRef = useRef(null);
|
||||
const repeatTimerRef = useRef(null);
|
||||
const keymap = useMemo(() => normalizeKeymapEntries(rawKeymap), [rawKeymap]);
|
||||
const keyboardSpeeds = useMemo(() => resolveKeyboardSpeeds(inputSettings), [inputSettings]);
|
||||
|
||||
const clearRepeatTimer = useCallback(() => {
|
||||
if (!repeatTimerRef.current) return;
|
||||
clearInterval(repeatTimerRef.current);
|
||||
repeatTimerRef.current = null;
|
||||
}, []);
|
||||
|
||||
const buildVirtualKeyTokens = useCallback(
|
||||
(cell, modeId = speedModeRef.current) => {
|
||||
const tokens = new Set();
|
||||
const speedModeConfig = getSpeedModeConfig(modeId);
|
||||
const actionIds = [
|
||||
...(Array.isArray(cell?.actions) ? cell.actions : []),
|
||||
speedModeConfig.modifierAction,
|
||||
].filter(Boolean);
|
||||
|
||||
actionIds.forEach((actionId) => {
|
||||
const token = firstTokenForAction(keymap, actionId);
|
||||
if (token) tokens.add(token);
|
||||
});
|
||||
|
||||
return tokens;
|
||||
},
|
||||
[keymap],
|
||||
);
|
||||
|
||||
const sendDriveCell = useCallback(
|
||||
(cell, lastEvent = 'move', modeId = speedModeRef.current) => {
|
||||
if (disabled) return;
|
||||
const tokens = buildVirtualKeyTokens(cell, modeId);
|
||||
const vector = computeKeyboardDriveVector(tokens, keymap);
|
||||
const speedOptions = getKeyboardDriveSpeedOptions(tokens, keymap, keyboardSpeeds);
|
||||
|
||||
// Mobile deliberately routes through the keyboard vector/speed helpers. The
|
||||
// thumb pad only chooses which virtual keys are down, so changes to keyboard
|
||||
// drive behavior automatically stay matched here.
|
||||
setDriveVector(vector, { source: SOURCE, speedOptions });
|
||||
registerInputState(SOURCE, {
|
||||
keys: Array.from(tokens),
|
||||
vector,
|
||||
activeCell: cell?.id ?? 'stop',
|
||||
speedMode: modeId,
|
||||
lastEvent,
|
||||
});
|
||||
},
|
||||
[
|
||||
buildVirtualKeyTokens,
|
||||
disabled,
|
||||
keyboardSpeeds,
|
||||
keymap,
|
||||
registerInputState,
|
||||
setDriveVector,
|
||||
],
|
||||
);
|
||||
|
||||
const stopDrivePad = useCallback(
|
||||
(lastEvent = 'stop') => {
|
||||
clearRepeatTimer();
|
||||
activeCellRef.current = null;
|
||||
setActiveInputLabel('stop');
|
||||
sendDriveCell({ id: 'stop', actions: [] }, lastEvent, 'normal');
|
||||
},
|
||||
[clearRepeatTimer, sendDriveCell],
|
||||
);
|
||||
|
||||
const startRepeatTimer = useCallback(() => {
|
||||
if (repeatTimerRef.current) return;
|
||||
repeatTimerRef.current = setInterval(() => {
|
||||
if (!activeCellRef.current) return;
|
||||
sendDriveCell(activeCellRef.current, 'repeat');
|
||||
}, DRIVE_PAD_REPEAT_MS);
|
||||
}, [sendDriveCell]);
|
||||
|
||||
const handleCellChange = useCallback(
|
||||
(cell) => {
|
||||
if (disabled) return;
|
||||
activeCellRef.current = cell;
|
||||
setActiveInputLabel(cell?.label || 'stop');
|
||||
sendDriveCell(cell, 'move');
|
||||
startRepeatTimer();
|
||||
},
|
||||
[disabled, sendDriveCell, startRepeatTimer],
|
||||
);
|
||||
|
||||
const handleSpeedModeChange = useCallback(
|
||||
(nextMode) => {
|
||||
setSpeedMode(nextMode);
|
||||
speedModeRef.current = nextMode;
|
||||
if (activeCellRef.current) {
|
||||
sendDriveCell(activeCellRef.current, 'speed', nextMode);
|
||||
}
|
||||
},
|
||||
[sendDriveCell],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => clearRepeatTimer();
|
||||
}, [clearRepeatTimer]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!disabled) return;
|
||||
stopDrivePad('disabled');
|
||||
}, [disabled, stopDrivePad]);
|
||||
|
||||
return (
|
||||
<div className="mobile-touch-control flex flex-1 min-h-0 flex-col overflow-hidden rounded-xl border-2 border-slate-700 bg-slate-900 text-slate-100 shadow-md">
|
||||
<div className="mobile-touch-control grid grid-cols-3 gap-0.5 border-b border-slate-700 bg-slate-950 p-0.5">
|
||||
{DRIVE_PAD_SPEED_MODES.map((mode) => {
|
||||
const active = speedMode === mode.id;
|
||||
const speedValue =
|
||||
mode.id === 'precision'
|
||||
? keyboardSpeeds.precisionSpeed
|
||||
: mode.id === 'turbo'
|
||||
? keyboardSpeeds.turboSpeed
|
||||
: keyboardSpeeds.baseSpeed;
|
||||
return (
|
||||
<button
|
||||
key={mode.id}
|
||||
type="button"
|
||||
className={`mobile-touch-control min-h-9 rounded-md px-1 text-xs font-semibold ${
|
||||
active
|
||||
? 'bg-cyan-300 text-slate-950'
|
||||
: 'bg-slate-800 text-slate-200'
|
||||
}`}
|
||||
onClick={() => handleSpeedModeChange(mode.id)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<span className="block leading-tight">{mode.label}</span>
|
||||
<span className="block font-mono text-[0.7rem] leading-tight">{speedValue}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="mobile-touch-control min-h-0 flex-1">
|
||||
<FloatingJoystick
|
||||
activeInputLabel={activeInputLabel}
|
||||
disabled={disabled}
|
||||
onCellChange={handleCellChange}
|
||||
onStop={() => stopDrivePad('stop')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user