This commit is contained in:
legop3
2025-11-17 23:48:58 -05:00
parent d0ea99bc4c
commit ffa4189763
10 changed files with 280 additions and 97 deletions
+32
View File
@@ -3,9 +3,17 @@ import { controlReducer, initialControlState } from './controlReducer.js';
import { computeDifferentialSpeeds, clamp } from './controlMath.js';
import { useCommandPipeline } from './commandPipeline.js';
import { loadControlSettings, saveControlSettings } from './persistence.js';
import { DEFAULT_KEYMAP } from './constants.js';
import { canonicalizeKeyInput } from './keymapUtils.js';
const ControlSystemContext = createContext(null);
function cloneKeymap(map) {
return Object.fromEntries(
Object.entries(map || {}).map(([key, values]) => [key, Array.isArray(values) ? [...values] : []]),
);
}
function clampServoAngle(config, value) {
if (!config) return value;
const min = typeof config.minAngle === 'number' ? config.minAngle : -45;
@@ -90,6 +98,26 @@ export function ControlSystemProvider({ children }) {
[pipeline],
);
const updateKeyBinding = useCallback(
(bindingId, keyValue) => {
if (!bindingId) return false;
const canonical = canonicalizeKeyInput(keyValue);
if (!canonical) return false;
const next = cloneKeymap(state.keymap);
next[bindingId] = [canonical];
dispatch({ type: 'control/set-keymap', payload: next });
persistSettings({ keymap: next });
return true;
},
[state.keymap, persistSettings],
);
const resetKeyBindings = useCallback(() => {
const defaults = cloneKeymap(DEFAULT_KEYMAP);
dispatch({ type: 'control/set-keymap', payload: defaults });
persistSettings({ keymap: defaults });
}, [persistSettings]);
const setServoAngle = useCallback(
(value) => {
if (!pipeline.servoConfig) return;
@@ -212,6 +240,8 @@ export function ControlSystemProvider({ children }) {
stopAllMotion,
sendOiCommand,
setSensorStream,
updateKeyBinding,
resetKeyBindings,
registerInputState,
reloadSettings,
persistSettings,
@@ -230,6 +260,8 @@ export function ControlSystemProvider({ children }) {
stopAllMotion,
sendOiCommand,
setSensorStream,
updateKeyBinding,
resetKeyBindings,
registerInputState,
reloadSettings,
persistSettings,
@@ -1,37 +1,11 @@
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { useControlSystem } from '../ControlContext.jsx';
import { normalizeKeymapEntries, tokensForEvent } from '../keymapUtils.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;
const KEY_ALIASES = {
'{': '[',
'}': ']',
':': ';',
'"': "'",
'<': ',',
'>': '.',
'?': '/',
'|': '\\',
'_': '-',
'+': '=',
};
const BINDING_CODE_MAP = {
'[': 'BracketLeft',
']': 'BracketRight',
'\\': 'Backslash',
';': 'Semicolon',
"'": 'Quote',
',': 'Comma',
'.': 'Period',
'/': 'Slash',
'-': 'Minus',
'=': 'Equal',
'`': 'Backquote',
};
const CODE_PREFIX = 'code:';
const KEY_PREFIX = 'key:';
function shouldIgnoreEvent(event) {
const target = event.target;
@@ -45,59 +19,6 @@ function shouldIgnoreEvent(event) {
);
}
function canonicalizeValue(value) {
if (typeof value !== 'string') return '';
const lower = value.toLowerCase();
return KEY_ALIASES[lower] ?? lower;
}
function deriveCodeForValue(value) {
if (!value) return null;
if (BINDING_CODE_MAP[value]) return BINDING_CODE_MAP[value];
if (value.length === 1) {
if (/[a-z]/.test(value)) return `Key${value.toUpperCase()}`;
if (/[0-9]/.test(value)) return `Digit${value}`;
}
return null;
}
function makeKeyToken(value) {
const canonical = canonicalizeValue(value);
return canonical ? `${KEY_PREFIX}${canonical}` : null;
}
function makeCodeToken(value) {
const canonical = canonicalizeValue(value);
const code = deriveCodeForValue(canonical);
return code ? `${CODE_PREFIX}${code}` : null;
}
function tokensFromEvent(event) {
const tokens = new Set();
const keyToken = makeKeyToken(event?.key ?? '');
if (keyToken) tokens.add(keyToken);
const code = event?.code;
if (code) {
tokens.add(`${CODE_PREFIX}${code}`);
}
return Array.from(tokens);
}
function normalizeKeymap(keymap = {}) {
const entries = Object.entries(keymap).map(([action, bindings]) => {
const values = Array.isArray(bindings) ? bindings : [bindings];
const normalized = new Set();
values.forEach((value) => {
const keyToken = makeKeyToken(String(value));
if (keyToken) normalized.add(keyToken);
const codeToken = makeCodeToken(String(value));
if (codeToken) normalized.add(codeToken);
});
return [action, normalized];
});
return Object.fromEntries(entries);
}
function bindingActive(bindingSet, keys) {
if (!bindingSet || bindingSet.size === 0) return false;
for (const key of keys) {
@@ -166,7 +87,7 @@ export default function KeyboardInputManager() {
registerInputState,
},
} = useControlSystem();
const keymap = useMemo(() => normalizeKeymap(state.keymap), [state.keymap]);
const keymap = useMemo(() => normalizeKeymapEntries(state.keymap), [state.keymap]);
const actionTokens = useMemo(() => {
const tokens = new Set();
Object.values(keymap).forEach((bindingSet) => {
@@ -271,7 +192,7 @@ export default function KeyboardInputManager() {
useEffect(() => {
function handleKeyDown(event) {
if (shouldIgnoreEvent(event)) return;
const tokens = tokensFromEvent(event);
const tokens = tokensForEvent(event);
if (tokens.length === 0) return;
if (tokens.some((token) => actionTokens.has(token))) {
event.preventDefault();
@@ -296,7 +217,7 @@ export default function KeyboardInputManager() {
}
function handleKeyUp(event) {
const tokens = tokensFromEvent(event);
const tokens = tokensForEvent(event);
// eslint-disable-next-line no-console
console.debug('[keyboard] keyup', { key: event.key, code: event.code, tokens });
tokens.forEach((token) => activeTokensRef.current.delete(token));
+96
View File
@@ -0,0 +1,96 @@
const KEY_ALIASES = {
'{': '[',
'}': ']',
':': ';',
'"': "'",
'<': ',',
'>': '.',
'?': '/',
'|': '\\',
'_': '-',
'+': '=',
};
const BINDING_CODE_MAP = {
'[': 'BracketLeft',
']': 'BracketRight',
'\\': 'Backslash',
';': 'Semicolon',
"'": 'Quote',
',': 'Comma',
'.': 'Period',
'/': 'Slash',
'-': 'Minus',
'=': 'Equal',
'`': 'Backquote',
};
export function canonicalizeKeyInput(value) {
if (typeof value !== 'string') return '';
const lower = value.toLowerCase();
return KEY_ALIASES[lower] ?? lower;
}
export function deriveCodeForKey(value) {
const canonical = canonicalizeKeyInput(value);
if (!canonical) return null;
if (BINDING_CODE_MAP[canonical]) return BINDING_CODE_MAP[canonical];
if (canonical.length === 1) {
if (/[a-z]/.test(canonical)) return `Key${canonical.toUpperCase()}`;
if (/[0-9]/.test(canonical)) return `Digit${canonical}`;
}
return null;
}
export function createKeyToken(value) {
const canonical = canonicalizeKeyInput(value);
return canonical ? `key:${canonical}` : null;
}
export function createCodeToken(value) {
const code = deriveCodeForKey(value);
return code ? `code:${code}` : null;
}
export function tokensForEvent(event) {
const tokens = new Set();
const keyToken = createKeyToken(event?.key ?? '');
if (keyToken) tokens.add(keyToken);
const codeToken = event?.code ? `code:${event.code}` : null;
if (codeToken) tokens.add(codeToken);
return Array.from(tokens);
}
export function normalizeKeymapEntries(keymap = {}) {
return Object.fromEntries(
Object.entries(keymap).map(([action, bindings]) => {
const values = Array.isArray(bindings) ? bindings : [bindings];
const normalized = new Set();
values.forEach((value) => {
const keyToken = createKeyToken(String(value));
if (keyToken) normalized.add(keyToken);
const codeToken = createCodeToken(String(value));
if (codeToken) normalized.add(codeToken);
});
return [action, normalized];
}),
);
}
export function formatKeyLabel(value) {
const canonical = canonicalizeKeyInput(value);
if (!canonical) return '—';
if (canonical === ' ') return 'Space';
if (canonical === '\\') return 'Backslash';
if (canonical === '`') return 'Backtick';
if (canonical === '[') return '[ or {';
if (canonical === ']') return '] or }';
if (canonical === ';') return '; or :';
if (canonical === "'") return "' or \"";
if (canonical === ',') return ', or <';
if (canonical === '.') return '. or >';
if (canonical === '/') return '/ or ?';
if (canonical === '-') return '- or _';
if (canonical === '=') return '= or +';
return canonical.length === 1 ? canonical.toUpperCase() : canonical;
}