mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
keyboard speed controls
This commit is contained in:
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
@@ -11,8 +11,8 @@
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
|
||||
<title>Multi Roomba Rover</title>
|
||||
<script type="module" crossorigin src="/assets/index-CB1QIk24.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-4BBPmLYK.css">
|
||||
<script type="module" crossorigin src="/assets/index-BCyA38CU.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DusXB4R9.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -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