mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
keyboard speed controls
This commit is contained in:
@@ -2,6 +2,8 @@ import { useMemo, useState, useEffect, useCallback } from 'react';
|
||||
import { useControlSystem } from '../controls/index.js';
|
||||
import { DEFAULT_KEYMAP } from '../controls/constants.js';
|
||||
import { canonicalizeKeyInput, formatKeyLabel } from '../controls/keymapUtils.js';
|
||||
import { useSettingsNamespace } from '../settings/index.js';
|
||||
import { INPUT_SETTINGS_DEFAULTS } from '../settings/namespaces.js';
|
||||
|
||||
const KEY_ACTIONS = [
|
||||
{ id: 'driveForward', label: 'Drive Forward', group: 'Driving' },
|
||||
@@ -33,6 +35,12 @@ function groupActions(actions) {
|
||||
}, {});
|
||||
}
|
||||
|
||||
function clampSpeed(value, fallback) {
|
||||
const num = Number(value);
|
||||
if (!Number.isFinite(num)) return fallback;
|
||||
return Math.max(0, Math.min(500, num));
|
||||
}
|
||||
|
||||
function useKeyCapture(onCapture) {
|
||||
const [active, setActive] = useState(null);
|
||||
|
||||
@@ -57,11 +65,44 @@ function useKeyCapture(onCapture) {
|
||||
return { active, startCapture: setActive, cancel: () => setActive(null) };
|
||||
}
|
||||
|
||||
function SpeedField({ label, description, value, onChange }) {
|
||||
return (
|
||||
<label className="surface-muted block p-0.5">
|
||||
<div className="flex items-center justify-between text-xs text-slate-300">
|
||||
<span className="font-semibold text-slate-100">{label}</span>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
max={500}
|
||||
value={value}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
className="w-16 rounded border border-slate-700 bg-slate-900 px-1 py-[2px] text-right text-[0.75rem] font-mono text-slate-100"
|
||||
/>
|
||||
</div>
|
||||
{description && <p className="text-[0.65rem] text-slate-500">{description}</p>}
|
||||
<div className="mt-0.5 flex items-center gap-0.5">
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={500}
|
||||
step={5}
|
||||
value={value}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
className="h-2 flex-1 accent-emerald-400"
|
||||
/>
|
||||
<span className="w-12 text-right text-[0.75rem] font-mono text-slate-400">{value}</span>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
export default function KeymapSettings() {
|
||||
const {
|
||||
state: { keymap },
|
||||
actions: { updateKeyBinding, resetKeyBindings },
|
||||
} = useControlSystem();
|
||||
const { value: inputSettings, save: saveInputSettings } = useSettingsNamespace('inputs', INPUT_SETTINGS_DEFAULTS);
|
||||
const keyboardSpeeds = inputSettings.keyboard ?? INPUT_SETTINGS_DEFAULTS.keyboard;
|
||||
const grouped = useMemo(() => groupActions(KEY_ACTIONS), []);
|
||||
const { active, startCapture, cancel } = useKeyCapture((actionId, value) => {
|
||||
updateKeyBinding(actionId, value);
|
||||
@@ -72,6 +113,21 @@ export default function KeymapSettings() {
|
||||
[keymap],
|
||||
);
|
||||
|
||||
const updateKeyboardSpeed = useCallback(
|
||||
(key, nextValue) => {
|
||||
const defaults = INPUT_SETTINGS_DEFAULTS.keyboard;
|
||||
const clamped = clampSpeed(nextValue, defaults[key]);
|
||||
saveInputSettings((prev) => ({
|
||||
...(prev ?? {}),
|
||||
keyboard: {
|
||||
...(prev?.keyboard ?? defaults),
|
||||
[key]: clamped,
|
||||
},
|
||||
}));
|
||||
},
|
||||
[saveInputSettings],
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="panel-section space-y-0.5 text-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -83,6 +139,29 @@ export default function KeymapSettings() {
|
||||
Reset defaults
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-0.5 surface">
|
||||
<p className="text-[0.7rem] text-slate-500">Keyboard speeds</p>
|
||||
<div className="space-y-0.5">
|
||||
<SpeedField
|
||||
label="Base speed"
|
||||
description="Normal driving speed"
|
||||
value={keyboardSpeeds.baseSpeed}
|
||||
onChange={(value) => updateKeyboardSpeed('baseSpeed', value)}
|
||||
/>
|
||||
<SpeedField
|
||||
label="Turbo speed"
|
||||
description="Used when holding the boost modifier"
|
||||
value={keyboardSpeeds.turboSpeed}
|
||||
onChange={(value) => updateKeyboardSpeed('turboSpeed', value)}
|
||||
/>
|
||||
<SpeedField
|
||||
label="Precision speed"
|
||||
description="Used when holding the precision modifier"
|
||||
value={keyboardSpeeds.precisionSpeed}
|
||||
onChange={(value) => updateKeyboardSpeed('precisionSpeed', value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
{Object.entries(grouped).map(([group, actions]) => (
|
||||
<div key={group} className="space-y-0.5 surface">
|
||||
|
||||
@@ -122,7 +122,7 @@ export function ControlSystemProvider({ children }) {
|
||||
|
||||
const setDriveVector = useCallback(
|
||||
(vector, meta = {}) => {
|
||||
const computed = computeDifferentialSpeeds(vector);
|
||||
const computed = computeDifferentialSpeeds(vector, meta.speedOptions);
|
||||
dispatch({
|
||||
type: 'control/update-drive',
|
||||
payload: { ...computed, source: meta.source ?? null },
|
||||
|
||||
@@ -2,12 +2,20 @@ import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useControlSystem } from '../ControlContext.jsx';
|
||||
import { useChat } from '../../context/ChatContext.jsx';
|
||||
import { normalizeKeymapEntries, tokensForEvent } from '../keymapUtils.js';
|
||||
import { useSettingsNamespace } from '../../settings/index.js';
|
||||
import { INPUT_SETTINGS_DEFAULTS } from '../../settings/namespaces.js';
|
||||
|
||||
const SOURCE = 'keyboard';
|
||||
const ZERO_VECTOR = { x: 0, y: 0, boost: false };
|
||||
const ZERO_AUX = { main: 0, side: 0, vacuum: 0 };
|
||||
const SERVO_REPEAT_MS = 110;
|
||||
|
||||
function clampSpeed(value, fallback) {
|
||||
const num = Number(value);
|
||||
if (!Number.isFinite(num)) return fallback;
|
||||
return Math.max(0, Math.min(500, num));
|
||||
}
|
||||
|
||||
function shouldIgnoreEvent(event) {
|
||||
const target = event.target;
|
||||
if (!target) return false;
|
||||
@@ -90,6 +98,7 @@ export default function KeyboardInputManager() {
|
||||
},
|
||||
} = useControlSystem();
|
||||
const { focusChat, blurChat, isChatFocused } = useChat();
|
||||
const { value: inputSettings } = useSettingsNamespace('inputs', INPUT_SETTINGS_DEFAULTS);
|
||||
const keymap = useMemo(() => normalizeKeymapEntries(state.keymap), [state.keymap]);
|
||||
const actionTokens = useMemo(() => {
|
||||
const tokens = new Set();
|
||||
@@ -99,6 +108,15 @@ export default function KeyboardInputManager() {
|
||||
});
|
||||
return tokens;
|
||||
}, [keymap]);
|
||||
const keyboardSpeeds = useMemo(() => {
|
||||
const defaults = INPUT_SETTINGS_DEFAULTS.keyboard;
|
||||
const current = inputSettings?.keyboard ?? {};
|
||||
return {
|
||||
baseSpeed: clampSpeed(current.baseSpeed, defaults.baseSpeed),
|
||||
turboSpeed: clampSpeed(current.turboSpeed, defaults.turboSpeed),
|
||||
precisionSpeed: clampSpeed(current.precisionSpeed, defaults.precisionSpeed),
|
||||
};
|
||||
}, [inputSettings?.keyboard]);
|
||||
const servoStep = useMemo(
|
||||
() => Math.abs(state.camera?.config?.nudgeDegrees || 1),
|
||||
[state.camera?.config?.nudgeDegrees],
|
||||
@@ -111,6 +129,11 @@ export default function KeyboardInputManager() {
|
||||
|
||||
const driveFromKeys = useCallback(() => {
|
||||
const tokensSnapshot = new Set(activeTokensRef.current);
|
||||
const boostActive = bindingActive(keymap.boostModifier, tokensSnapshot);
|
||||
const slowActive = bindingActive(keymap.slowModifier, tokensSnapshot);
|
||||
const speedOptions = slowActive
|
||||
? { baseSpeed: keyboardSpeeds.precisionSpeed, boostSpeed: keyboardSpeeds.precisionSpeed }
|
||||
: { baseSpeed: keyboardSpeeds.baseSpeed, boostSpeed: keyboardSpeeds.turboSpeed };
|
||||
const vector = computeDriveVector(tokensSnapshot, keymap);
|
||||
const aux = computeAuxMotors(tokensSnapshot, keymap);
|
||||
if (
|
||||
@@ -119,7 +142,7 @@ export default function KeyboardInputManager() {
|
||||
vector.boost !== lastVectorRef.current.boost
|
||||
) {
|
||||
lastVectorRef.current = vector;
|
||||
setDriveVector(vector, { source: SOURCE });
|
||||
setDriveVector(vector, { source: SOURCE, speedOptions });
|
||||
}
|
||||
if (
|
||||
aux.main !== lastAuxRef.current.main ||
|
||||
@@ -134,7 +157,7 @@ export default function KeyboardInputManager() {
|
||||
vector,
|
||||
aux,
|
||||
});
|
||||
}, [keymap, registerInputState, setAuxMotors, setDriveVector]);
|
||||
}, [keymap, keyboardSpeeds, registerInputState, setAuxMotors, setDriveVector]);
|
||||
|
||||
const stopServoLoop = useCallback(() => {
|
||||
if (servoIntervalRef.current) {
|
||||
|
||||
@@ -35,7 +35,7 @@ export const HELP_CONTENT = {
|
||||
{
|
||||
type: 'keyboard',
|
||||
title: 'Keyboard controls',
|
||||
footnote: 'Per-browser; adjust in Settings → Keybindings.',
|
||||
footnote: 'Per-browser; adjust bindings and speeds in Settings → Keybindings.',
|
||||
groups: [
|
||||
{
|
||||
id: 'movement',
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
export const INPUT_SETTINGS_DEFAULTS = {
|
||||
keyboard: {
|
||||
baseSpeed: 250,
|
||||
turboSpeed: 400,
|
||||
precisionSpeed: 125,
|
||||
},
|
||||
gamepad: {
|
||||
driveDeadzone: 0.2,
|
||||
cameraDeadzone: 0.25,
|
||||
|
||||
Reference in New Issue
Block a user