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:
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-status-bar-style" content="black-translucent" />
|
||||||
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
|
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
|
||||||
<title>Multi Roomba Rover</title>
|
<title>Multi Roomba Rover</title>
|
||||||
<script type="module" crossorigin src="/assets/index-CB1QIk24.js"></script>
|
<script type="module" crossorigin src="/assets/index-BCyA38CU.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-4BBPmLYK.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-DusXB4R9.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { useMemo, useState, useEffect, useCallback } from 'react';
|
|||||||
import { useControlSystem } from '../controls/index.js';
|
import { useControlSystem } from '../controls/index.js';
|
||||||
import { DEFAULT_KEYMAP } from '../controls/constants.js';
|
import { DEFAULT_KEYMAP } from '../controls/constants.js';
|
||||||
import { canonicalizeKeyInput, formatKeyLabel } from '../controls/keymapUtils.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 = [
|
const KEY_ACTIONS = [
|
||||||
{ id: 'driveForward', label: 'Drive Forward', group: 'Driving' },
|
{ 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) {
|
function useKeyCapture(onCapture) {
|
||||||
const [active, setActive] = useState(null);
|
const [active, setActive] = useState(null);
|
||||||
|
|
||||||
@@ -57,11 +65,44 @@ function useKeyCapture(onCapture) {
|
|||||||
return { active, startCapture: setActive, cancel: () => setActive(null) };
|
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() {
|
export default function KeymapSettings() {
|
||||||
const {
|
const {
|
||||||
state: { keymap },
|
state: { keymap },
|
||||||
actions: { updateKeyBinding, resetKeyBindings },
|
actions: { updateKeyBinding, resetKeyBindings },
|
||||||
} = useControlSystem();
|
} = 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 grouped = useMemo(() => groupActions(KEY_ACTIONS), []);
|
||||||
const { active, startCapture, cancel } = useKeyCapture((actionId, value) => {
|
const { active, startCapture, cancel } = useKeyCapture((actionId, value) => {
|
||||||
updateKeyBinding(actionId, value);
|
updateKeyBinding(actionId, value);
|
||||||
@@ -72,6 +113,21 @@ export default function KeymapSettings() {
|
|||||||
[keymap],
|
[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 (
|
return (
|
||||||
<section className="panel-section space-y-0.5 text-sm">
|
<section className="panel-section space-y-0.5 text-sm">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -83,6 +139,29 @@ export default function KeymapSettings() {
|
|||||||
Reset defaults
|
Reset defaults
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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">
|
<div className="space-y-0.5">
|
||||||
{Object.entries(grouped).map(([group, actions]) => (
|
{Object.entries(grouped).map(([group, actions]) => (
|
||||||
<div key={group} className="space-y-0.5 surface">
|
<div key={group} className="space-y-0.5 surface">
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ export function ControlSystemProvider({ children }) {
|
|||||||
|
|
||||||
const setDriveVector = useCallback(
|
const setDriveVector = useCallback(
|
||||||
(vector, meta = {}) => {
|
(vector, meta = {}) => {
|
||||||
const computed = computeDifferentialSpeeds(vector);
|
const computed = computeDifferentialSpeeds(vector, meta.speedOptions);
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'control/update-drive',
|
type: 'control/update-drive',
|
||||||
payload: { ...computed, source: meta.source ?? null },
|
payload: { ...computed, source: meta.source ?? null },
|
||||||
|
|||||||
@@ -2,12 +2,20 @@ import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|||||||
import { useControlSystem } from '../ControlContext.jsx';
|
import { useControlSystem } from '../ControlContext.jsx';
|
||||||
import { useChat } from '../../context/ChatContext.jsx';
|
import { useChat } from '../../context/ChatContext.jsx';
|
||||||
import { normalizeKeymapEntries, tokensForEvent } from '../keymapUtils.js';
|
import { normalizeKeymapEntries, tokensForEvent } from '../keymapUtils.js';
|
||||||
|
import { useSettingsNamespace } from '../../settings/index.js';
|
||||||
|
import { INPUT_SETTINGS_DEFAULTS } from '../../settings/namespaces.js';
|
||||||
|
|
||||||
const SOURCE = 'keyboard';
|
const SOURCE = 'keyboard';
|
||||||
const ZERO_VECTOR = { x: 0, y: 0, boost: false };
|
const ZERO_VECTOR = { x: 0, y: 0, boost: false };
|
||||||
const ZERO_AUX = { main: 0, side: 0, vacuum: 0 };
|
const ZERO_AUX = { main: 0, side: 0, vacuum: 0 };
|
||||||
const SERVO_REPEAT_MS = 110;
|
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) {
|
function shouldIgnoreEvent(event) {
|
||||||
const target = event.target;
|
const target = event.target;
|
||||||
if (!target) return false;
|
if (!target) return false;
|
||||||
@@ -90,6 +98,7 @@ export default function KeyboardInputManager() {
|
|||||||
},
|
},
|
||||||
} = useControlSystem();
|
} = useControlSystem();
|
||||||
const { focusChat, blurChat, isChatFocused } = useChat();
|
const { focusChat, blurChat, isChatFocused } = useChat();
|
||||||
|
const { value: inputSettings } = useSettingsNamespace('inputs', INPUT_SETTINGS_DEFAULTS);
|
||||||
const keymap = useMemo(() => normalizeKeymapEntries(state.keymap), [state.keymap]);
|
const keymap = useMemo(() => normalizeKeymapEntries(state.keymap), [state.keymap]);
|
||||||
const actionTokens = useMemo(() => {
|
const actionTokens = useMemo(() => {
|
||||||
const tokens = new Set();
|
const tokens = new Set();
|
||||||
@@ -99,6 +108,15 @@ export default function KeyboardInputManager() {
|
|||||||
});
|
});
|
||||||
return tokens;
|
return tokens;
|
||||||
}, [keymap]);
|
}, [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(
|
const servoStep = useMemo(
|
||||||
() => Math.abs(state.camera?.config?.nudgeDegrees || 1),
|
() => Math.abs(state.camera?.config?.nudgeDegrees || 1),
|
||||||
[state.camera?.config?.nudgeDegrees],
|
[state.camera?.config?.nudgeDegrees],
|
||||||
@@ -111,6 +129,11 @@ export default function KeyboardInputManager() {
|
|||||||
|
|
||||||
const driveFromKeys = useCallback(() => {
|
const driveFromKeys = useCallback(() => {
|
||||||
const tokensSnapshot = new Set(activeTokensRef.current);
|
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 vector = computeDriveVector(tokensSnapshot, keymap);
|
||||||
const aux = computeAuxMotors(tokensSnapshot, keymap);
|
const aux = computeAuxMotors(tokensSnapshot, keymap);
|
||||||
if (
|
if (
|
||||||
@@ -119,7 +142,7 @@ export default function KeyboardInputManager() {
|
|||||||
vector.boost !== lastVectorRef.current.boost
|
vector.boost !== lastVectorRef.current.boost
|
||||||
) {
|
) {
|
||||||
lastVectorRef.current = vector;
|
lastVectorRef.current = vector;
|
||||||
setDriveVector(vector, { source: SOURCE });
|
setDriveVector(vector, { source: SOURCE, speedOptions });
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
aux.main !== lastAuxRef.current.main ||
|
aux.main !== lastAuxRef.current.main ||
|
||||||
@@ -134,7 +157,7 @@ export default function KeyboardInputManager() {
|
|||||||
vector,
|
vector,
|
||||||
aux,
|
aux,
|
||||||
});
|
});
|
||||||
}, [keymap, registerInputState, setAuxMotors, setDriveVector]);
|
}, [keymap, keyboardSpeeds, registerInputState, setAuxMotors, setDriveVector]);
|
||||||
|
|
||||||
const stopServoLoop = useCallback(() => {
|
const stopServoLoop = useCallback(() => {
|
||||||
if (servoIntervalRef.current) {
|
if (servoIntervalRef.current) {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export const HELP_CONTENT = {
|
|||||||
{
|
{
|
||||||
type: 'keyboard',
|
type: 'keyboard',
|
||||||
title: 'Keyboard controls',
|
title: 'Keyboard controls',
|
||||||
footnote: 'Per-browser; adjust in Settings → Keybindings.',
|
footnote: 'Per-browser; adjust bindings and speeds in Settings → Keybindings.',
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
id: 'movement',
|
id: 'movement',
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
export const INPUT_SETTINGS_DEFAULTS = {
|
export const INPUT_SETTINGS_DEFAULTS = {
|
||||||
|
keyboard: {
|
||||||
|
baseSpeed: 250,
|
||||||
|
turboSpeed: 400,
|
||||||
|
precisionSpeed: 125,
|
||||||
|
},
|
||||||
gamepad: {
|
gamepad: {
|
||||||
driveDeadzone: 0.2,
|
driveDeadzone: 0.2,
|
||||||
cameraDeadzone: 0.25,
|
cameraDeadzone: 0.25,
|
||||||
|
|||||||
Reference in New Issue
Block a user