keyboard speed controls

This commit is contained in:
legop3
2025-12-06 01:07:01 -05:00
parent 2b1122dce9
commit b72c58452e
10 changed files with 126 additions and 19 deletions
+79
View File
@@ -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">
+1 -1
View File
@@ -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) {
+1 -1
View File
@@ -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',
+5
View File
@@ -1,4 +1,9 @@
export const INPUT_SETTINGS_DEFAULTS = {
keyboard: {
baseSpeed: 250,
turboSpeed: 400,
precisionSpeed: 125,
},
gamepad: {
driveDeadzone: 0.2,
cameraDeadzone: 0.25,