mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-17 01:50:47 -04:00
guh
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
@@ -5,8 +5,8 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>webui</title>
|
<title>webui</title>
|
||||||
<script type="module" crossorigin src="/assets/index-De1-bHUg.js"></script>
|
<script type="module" crossorigin src="/assets/index-Bsnm9v3l.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-BMozRdoG.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-CkUABVeV.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
+3
-1
@@ -80,11 +80,12 @@ function MobilePortraitLayout() {
|
|||||||
function MobileLandscapeLayout() {
|
function MobileLandscapeLayout() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<section className="grid grid-cols-[minmax(0,0.7fr)_minmax(0,2.1fr)_minmax(0,0.7fr)] gap-1">
|
<section className="grid min-h-screen grid-cols-[minmax(0,0.7fr)_minmax(0,2.1fr)_minmax(0,0.7fr)] gap-1">
|
||||||
<MobileLandscapeAuxColumn />
|
<MobileLandscapeAuxColumn />
|
||||||
<DriverVideoPanel />
|
<DriverVideoPanel />
|
||||||
<MobileLandscapeControlColumn />
|
<MobileLandscapeControlColumn />
|
||||||
</section>
|
</section>
|
||||||
|
<div className="flex flex-col gap-1 pb-2">
|
||||||
<RoomCameraPanel />
|
<RoomCameraPanel />
|
||||||
<DrivePanel />
|
<DrivePanel />
|
||||||
<section className="grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)] gap-1">
|
<section className="grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)] gap-1">
|
||||||
@@ -98,6 +99,7 @@ function MobileLandscapeLayout() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,134 @@
|
|||||||
import { useCallback, useRef } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { Joystick } from 'react-joystick-component';
|
|
||||||
import { useControlSystem } from '../controls/index.js';
|
import { useControlSystem } from '../controls/index.js';
|
||||||
import { clampUnit } from '../controls/controlMath.js';
|
import { clampUnit } from '../controls/controlMath.js';
|
||||||
import DriveModeToggle from './controls/DriveModeToggle.jsx';
|
import DriveModeToggle from './controls/DriveModeToggle.jsx';
|
||||||
|
|
||||||
const SOURCE = 'mobile-joystick';
|
const SOURCE = 'mobile-joystick';
|
||||||
|
const JOYSTICK_RADIUS = 80;
|
||||||
const AUX_BUTTONS = [
|
const AUX_BUTTONS = [
|
||||||
{ id: 'main-forward', label: 'Main +', values: { main: 127 }, hold: true },
|
{ id: 'main-forward', label: 'Main +', values: { main: 127 }, hold: true, color: 'bg-emerald-600' },
|
||||||
{ id: 'main-reverse', label: 'Main -', values: { main: -127 }, hold: true },
|
{ id: 'main-reverse', label: 'Main -', values: { main: -127 }, hold: true, color: 'bg-emerald-800' },
|
||||||
{ id: 'side-forward', label: 'Side +', values: { side: 127 }, hold: true },
|
{ id: 'side-forward', label: 'Side +', values: { side: 127 }, hold: true, color: 'bg-cyan-600' },
|
||||||
{ id: 'side-reverse', label: 'Side -', values: { side: -70 }, hold: true },
|
{ id: 'side-reverse', label: 'Side -', values: { side: -70 }, hold: true, color: 'bg-cyan-800' },
|
||||||
{ id: 'vacuum-fast', label: 'Vacuum Max', values: { vacuum: 127 }, hold: true },
|
{ id: 'vacuum-fast', label: 'Vacuum Max', values: { vacuum: 127 }, hold: true, color: 'bg-amber-500 text-amber-950' },
|
||||||
{ id: 'vacuum-slow', label: 'Vacuum Low', values: { vacuum: 50 }, hold: true },
|
{ id: 'vacuum-slow', label: 'Vacuum Low', values: { vacuum: 50 }, hold: true, color: 'bg-amber-700' },
|
||||||
{ id: 'all-forward', label: 'All +', values: { main: 127, side: 127, vacuum: 127 }, hold: true },
|
{ id: 'all-forward', label: 'All +', values: { main: 127, side: 127, vacuum: 127 }, hold: true, color: 'bg-fuchsia-600' },
|
||||||
{ id: 'stop-all', label: 'Stop', values: { main: 0, side: 0, vacuum: 0 }, hold: false },
|
{ id: 'stop-all', label: 'Stop', values: { main: 0, side: 0, vacuum: 0 }, hold: false, color: 'bg-slate-900' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
function FloatingJoystick({ disabled, layout, onMove, onStop }) {
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
const pointerIdRef = useRef(null);
|
||||||
|
const baseRef = useRef({ x: 0, y: 0 });
|
||||||
|
const [visual, setVisual] = useState({ active: false, base: { x: 0, y: 0 }, knob: { x: 0, y: 0 } });
|
||||||
|
|
||||||
|
const stopTracking = useCallback(() => {
|
||||||
|
pointerIdRef.current = null;
|
||||||
|
setVisual({ active: false, base: { x: 0, y: 0 }, knob: { x: 0, y: 0 } });
|
||||||
|
onStop?.();
|
||||||
|
}, [onStop]);
|
||||||
|
|
||||||
|
const handlePointerDown = useCallback(
|
||||||
|
(event) => {
|
||||||
|
if (disabled) return;
|
||||||
|
if (pointerIdRef.current !== null) return;
|
||||||
|
event.preventDefault();
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
const x = event.clientX - rect.left;
|
||||||
|
const y = event.clientY - rect.top;
|
||||||
|
baseRef.current = { x, y };
|
||||||
|
pointerIdRef.current = event.pointerId;
|
||||||
|
container.setPointerCapture?.(event.pointerId);
|
||||||
|
setVisual({ active: true, base: { x, y }, knob: { x: 0, y: 0 } });
|
||||||
|
},
|
||||||
|
[disabled],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePointerMove = useCallback(
|
||||||
|
(event) => {
|
||||||
|
if (disabled || pointerIdRef.current !== event.pointerId) return;
|
||||||
|
event.preventDefault();
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
const currentX = event.clientX - rect.left;
|
||||||
|
const currentY = event.clientY - rect.top;
|
||||||
|
const dx = currentX - baseRef.current.x;
|
||||||
|
const dy = currentY - baseRef.current.y;
|
||||||
|
const distance = Math.min(Math.hypot(dx, dy), JOYSTICK_RADIUS);
|
||||||
|
const angle = Math.atan2(dy, dx);
|
||||||
|
const knobX = Math.cos(angle) * distance;
|
||||||
|
const knobY = Math.sin(angle) * distance;
|
||||||
|
const vector = {
|
||||||
|
x: clampUnit(knobX / JOYSTICK_RADIUS),
|
||||||
|
y: clampUnit(-knobY / JOYSTICK_RADIUS),
|
||||||
|
boost: false,
|
||||||
|
};
|
||||||
|
setVisual((prev) => ({ ...prev, knob: { x: knobX, y: knobY } }));
|
||||||
|
onMove?.(vector);
|
||||||
|
},
|
||||||
|
[disabled, onMove],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePointerEnd = useCallback(
|
||||||
|
(event) => {
|
||||||
|
if (pointerIdRef.current !== event.pointerId) return;
|
||||||
|
event.preventDefault();
|
||||||
|
const container = containerRef.current;
|
||||||
|
container?.releasePointerCapture?.(event.pointerId);
|
||||||
|
stopTracking();
|
||||||
|
},
|
||||||
|
[stopTracking],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (disabled) {
|
||||||
|
stopTracking();
|
||||||
|
}
|
||||||
|
}, [disabled, stopTracking]);
|
||||||
|
|
||||||
|
const heightClass = layout === 'landscape' ? 'h-[260px]' : 'h-[220px]';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
role="presentation"
|
||||||
|
className={`relative w-full ${heightClass} select-none overflow-hidden rounded-lg border border-slate-800 bg-slate-900/70`}
|
||||||
|
style={{ touchAction: 'none' }}
|
||||||
|
onPointerDown={handlePointerDown}
|
||||||
|
onPointerMove={handlePointerMove}
|
||||||
|
onPointerUp={handlePointerEnd}
|
||||||
|
onPointerLeave={handlePointerEnd}
|
||||||
|
onPointerCancel={handlePointerEnd}
|
||||||
|
onContextMenu={(event) => event.preventDefault()}
|
||||||
|
>
|
||||||
|
{!visual.active && (
|
||||||
|
<div className="absolute inset-0 flex flex-col items-center justify-center text-center text-xs text-slate-400">
|
||||||
|
<p>Touch and hold anywhere</p>
|
||||||
|
<p>Joystick will follow your thumb</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{visual.active && (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="pointer-events-none absolute h-28 w-28 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-cyan-400/60 bg-cyan-400/10"
|
||||||
|
style={{ left: visual.base.x, top: visual.base.y }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="pointer-events-none absolute h-12 w-12 -translate-x-1/2 -translate-y-1/2 rounded-full bg-cyan-300/80 shadow-lg"
|
||||||
|
style={{
|
||||||
|
left: visual.base.x + visual.knob.x,
|
||||||
|
top: visual.base.y + visual.knob.y,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function MobileJoystickPanel({ layout }) {
|
function MobileJoystickPanel({ layout }) {
|
||||||
const {
|
const {
|
||||||
state: { roverId },
|
state: { roverId },
|
||||||
@@ -24,15 +137,15 @@ function MobileJoystickPanel({ layout }) {
|
|||||||
const disabled = !roverId;
|
const disabled = !roverId;
|
||||||
|
|
||||||
const handleMove = useCallback(
|
const handleMove = useCallback(
|
||||||
(event = {}) => {
|
(vector = {}) => {
|
||||||
if (disabled) return;
|
if (disabled) return;
|
||||||
const vector = {
|
const next = {
|
||||||
x: clampUnit(event.x ?? 0),
|
x: clampUnit(vector.x ?? 0),
|
||||||
y: clampUnit(event.y ?? 0),
|
y: clampUnit(vector.y ?? 0),
|
||||||
boost: Boolean(event.shiftKey),
|
boost: Boolean(vector.boost),
|
||||||
};
|
};
|
||||||
setDriveVector(vector, { source: SOURCE });
|
setDriveVector(next, { source: SOURCE });
|
||||||
registerInputState(SOURCE, { vector, lastEvent: 'move' });
|
registerInputState(SOURCE, { vector: next, lastEvent: 'move' });
|
||||||
},
|
},
|
||||||
[disabled, registerInputState, setDriveVector],
|
[disabled, registerInputState, setDriveVector],
|
||||||
);
|
);
|
||||||
@@ -45,31 +158,18 @@ function MobileJoystickPanel({ layout }) {
|
|||||||
}, [disabled, registerInputState, setDriveVector]);
|
}, [disabled, registerInputState, setDriveVector]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="rounded-sm bg-[#242a32] p-1 text-sm text-slate-100">
|
<div className="flex flex-col gap-1 text-slate-100">
|
||||||
<DriveModeToggle size="compact" />
|
<DriveModeToggle size="compact" />
|
||||||
<div className="mt-1 flex flex-col gap-1">
|
<FloatingJoystick disabled={disabled} layout={layout} onMove={handleMove} onStop={handleStop} />
|
||||||
<div className="flex items-center justify-center">
|
|
||||||
<Joystick
|
|
||||||
size={layout === 'landscape' ? 140 : 120}
|
|
||||||
baseColor="#0f172a"
|
|
||||||
stickColor="#38bdf8"
|
|
||||||
throttle={60}
|
|
||||||
move={handleMove}
|
|
||||||
stop={handleStop}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p className="text-[0.7rem] text-slate-400">Drag to steer · Release to stop sending drive commands.</p>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={stopAllMotion}
|
onClick={stopAllMotion}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className="rounded-sm bg-red-600 px-1 py-1 text-xs font-semibold uppercase tracking-wide text-red-100 disabled:opacity-40"
|
className="rounded bg-red-600 px-2 py-2 text-xs font-semibold uppercase tracking-wide text-red-100 disabled:opacity-40"
|
||||||
>
|
>
|
||||||
Panic Stop
|
Panic Stop
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,12 +206,12 @@ function AuxMotorPanel({ orientation }) {
|
|||||||
[disabled, setAuxMotors],
|
[disabled, setAuxMotors],
|
||||||
);
|
);
|
||||||
|
|
||||||
const buttonClasses = orientation === 'landscape' ? 'text-xs' : 'text-[0.75rem]';
|
const gridCols = orientation === 'landscape' ? 'grid-cols-1' : 'grid-cols-1 sm:grid-cols-2';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="rounded-sm bg-[#242a32] p-1 text-sm text-slate-100">
|
<div className="flex flex-col gap-1 text-slate-100">
|
||||||
<p className="text-xs text-slate-400">Aux motors · hold to run</p>
|
<p className="text-xs font-semibold uppercase tracking-wide text-slate-400">Aux controls</p>
|
||||||
<div className="mt-1 grid grid-cols-2 gap-1">
|
<div className={`grid ${gridCols} gap-1`}>
|
||||||
{AUX_BUTTONS.map((button) => (
|
{AUX_BUTTONS.map((button) => (
|
||||||
<button
|
<button
|
||||||
key={button.id}
|
key={button.id}
|
||||||
@@ -124,19 +224,20 @@ function AuxMotorPanel({ orientation }) {
|
|||||||
onPointerUp={() => handleRelease(button)}
|
onPointerUp={() => handleRelease(button)}
|
||||||
onPointerLeave={() => handleRelease(button)}
|
onPointerLeave={() => handleRelease(button)}
|
||||||
onPointerCancel={() => handleRelease(button)}
|
onPointerCancel={() => handleRelease(button)}
|
||||||
className={`rounded-sm bg-black/40 px-1 py-1 text-left text-slate-100 disabled:opacity-30 ${buttonClasses}`}
|
onContextMenu={(event) => event.preventDefault()}
|
||||||
|
className={`rounded-lg px-2 py-2 text-left text-sm font-semibold uppercase tracking-wide text-white transition ${button.color} disabled:opacity-30`}
|
||||||
>
|
>
|
||||||
{button.label}
|
{button.label}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MobileLandscapeAuxColumn() {
|
export function MobileLandscapeAuxColumn() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex h-full flex-col gap-1">
|
||||||
<AuxMotorPanel orientation="landscape" />
|
<AuxMotorPanel orientation="landscape" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -144,7 +245,7 @@ export function MobileLandscapeAuxColumn() {
|
|||||||
|
|
||||||
export function MobileLandscapeControlColumn() {
|
export function MobileLandscapeControlColumn() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex h-full flex-col gap-1">
|
||||||
<MobileJoystickPanel layout="landscape" />
|
<MobileJoystickPanel layout="landscape" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -152,8 +253,8 @@ export function MobileLandscapeControlColumn() {
|
|||||||
|
|
||||||
export default function MobilePortraitControls() {
|
export default function MobilePortraitControls() {
|
||||||
return (
|
return (
|
||||||
<section className="rounded-sm bg-transparent">
|
<section className="w-full">
|
||||||
<div className="grid grid-cols-1 gap-1 sm:grid-cols-2">
|
<div className="grid grid-cols-2 gap-1">
|
||||||
<AuxMotorPanel orientation="portrait" />
|
<AuxMotorPanel orientation="portrait" />
|
||||||
<MobileJoystickPanel layout="portrait" />
|
<MobileJoystickPanel layout="portrait" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useControlSystem } from '../ControlContext.jsx';
|
|||||||
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;
|
||||||
|
|
||||||
function shouldIgnoreEvent(event) {
|
function shouldIgnoreEvent(event) {
|
||||||
const target = event.target;
|
const target = event.target;
|
||||||
@@ -118,8 +119,19 @@ export default function KeyboardInputManager() {
|
|||||||
const pressedKeysRef = useRef(new Set());
|
const pressedKeysRef = useRef(new Set());
|
||||||
const lastVectorRef = useRef(ZERO_VECTOR);
|
const lastVectorRef = useRef(ZERO_VECTOR);
|
||||||
const lastAuxRef = useRef(ZERO_AUX);
|
const lastAuxRef = useRef(ZERO_AUX);
|
||||||
|
const cameraHoldRef = useRef(new Set());
|
||||||
|
const servoIntervalRef = useRef(null);
|
||||||
|
|
||||||
const updateFromKeys = useCallback(() => {
|
const actionKeys = useMemo(() => {
|
||||||
|
const entries = Object.values(keymap);
|
||||||
|
const set = new Set();
|
||||||
|
entries.forEach((bindingSet) => {
|
||||||
|
bindingSet?.forEach((binding) => set.add(binding));
|
||||||
|
});
|
||||||
|
return set;
|
||||||
|
}, [keymap]);
|
||||||
|
|
||||||
|
const driveFromKeys = useCallback(() => {
|
||||||
const keys = pressedKeysRef.current;
|
const keys = pressedKeysRef.current;
|
||||||
const vector = computeDriveVector(keys, keymap);
|
const vector = computeDriveVector(keys, keymap);
|
||||||
const aux = computeAuxMotors(keys, keymap);
|
const aux = computeAuxMotors(keys, keymap);
|
||||||
@@ -138,46 +150,100 @@ export default function KeyboardInputManager() {
|
|||||||
});
|
});
|
||||||
}, [keymap, registerInputState, setAuxMotors, setDriveVector]);
|
}, [keymap, registerInputState, setAuxMotors, setDriveVector]);
|
||||||
|
|
||||||
|
const stopServoLoop = useCallback(() => {
|
||||||
|
if (servoIntervalRef.current) {
|
||||||
|
clearTimeout(servoIntervalRef.current);
|
||||||
|
servoIntervalRef.current = null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const startServoLoop = useCallback(() => {
|
||||||
|
if (servoIntervalRef.current) return;
|
||||||
|
const tick = () => {
|
||||||
|
const up = cameraHoldRef.current.has('up') ? 1 : 0;
|
||||||
|
const down = cameraHoldRef.current.has('down') ? 1 : 0;
|
||||||
|
const direction = up - down;
|
||||||
|
if (direction === 0) {
|
||||||
|
servoIntervalRef.current = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
nudgeServo(direction * servoStep);
|
||||||
|
servoIntervalRef.current = setTimeout(tick, SERVO_REPEAT_MS);
|
||||||
|
};
|
||||||
|
servoIntervalRef.current = setTimeout(tick, 0);
|
||||||
|
}, [nudgeServo, servoStep]);
|
||||||
|
|
||||||
|
const updateServoHold = useCallback(() => {
|
||||||
|
if (cameraHoldRef.current.size === 0) {
|
||||||
|
stopServoLoop();
|
||||||
|
} else {
|
||||||
|
startServoLoop();
|
||||||
|
}
|
||||||
|
}, [startServoLoop, stopServoLoop]);
|
||||||
|
|
||||||
const resetAll = useCallback(() => {
|
const resetAll = useCallback(() => {
|
||||||
pressedKeysRef.current.clear();
|
pressedKeysRef.current.clear();
|
||||||
|
cameraHoldRef.current.clear();
|
||||||
|
stopServoLoop();
|
||||||
lastVectorRef.current = ZERO_VECTOR;
|
lastVectorRef.current = ZERO_VECTOR;
|
||||||
lastAuxRef.current = ZERO_AUX;
|
lastAuxRef.current = ZERO_AUX;
|
||||||
stopAllMotion();
|
stopAllMotion();
|
||||||
registerInputState(SOURCE, { keys: [], vector: ZERO_VECTOR, aux: ZERO_AUX });
|
registerInputState(SOURCE, { keys: [], vector: ZERO_VECTOR, aux: ZERO_AUX });
|
||||||
}, [registerInputState, stopAllMotion]);
|
}, [registerInputState, stopAllMotion, stopServoLoop]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleKeyDown(event) {
|
function handleKeyDown(event) {
|
||||||
if (shouldIgnoreEvent(event)) return;
|
if (shouldIgnoreEvent(event)) return;
|
||||||
const key = event.key?.toLowerCase();
|
const key = event.key?.toLowerCase();
|
||||||
if (!key) return;
|
if (!key) return;
|
||||||
if (pressedKeysRef.current.has(key)) return;
|
if (actionKeys.has(key)) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
if (!pressedKeysRef.current.has(key)) {
|
||||||
pressedKeysRef.current.add(key);
|
pressedKeysRef.current.add(key);
|
||||||
|
}
|
||||||
|
|
||||||
if (bindingHas(keymap.driveMacro, key)) {
|
if (bindingHas(keymap.driveMacro, key)) {
|
||||||
event.preventDefault();
|
|
||||||
setMode('drive');
|
setMode('drive');
|
||||||
runMacro('drive-sequence');
|
runMacro('drive-sequence');
|
||||||
} else if (bindingHas(keymap.dockMacro, key)) {
|
} else if (bindingHas(keymap.dockMacro, key)) {
|
||||||
event.preventDefault();
|
|
||||||
setMode('dock');
|
setMode('dock');
|
||||||
runMacro('seek-dock');
|
runMacro('seek-dock');
|
||||||
} else if (bindingHas(keymap.cameraUp, key)) {
|
} else if (bindingHas(keymap.cameraUp, key)) {
|
||||||
event.preventDefault();
|
cameraHoldRef.current.add('up');
|
||||||
nudgeServo(servoStep);
|
updateServoHold();
|
||||||
} else if (bindingHas(keymap.cameraDown, key)) {
|
} else if (bindingHas(keymap.cameraDown, key)) {
|
||||||
event.preventDefault();
|
cameraHoldRef.current.add('down');
|
||||||
nudgeServo(-servoStep);
|
updateServoHold();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFromKeys();
|
driveFromKeys();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKeyUp(event) {
|
function handleKeyUp(event) {
|
||||||
const key = event.key?.toLowerCase();
|
const key = event.key?.toLowerCase();
|
||||||
if (!key || !pressedKeysRef.current.has(key)) return;
|
if (!key || !pressedKeysRef.current.has(key)) {
|
||||||
|
if (bindingHas(keymap.cameraUp, key)) {
|
||||||
|
cameraHoldRef.current.delete('up');
|
||||||
|
updateServoHold();
|
||||||
|
} else if (bindingHas(keymap.cameraDown, key)) {
|
||||||
|
cameraHoldRef.current.delete('down');
|
||||||
|
updateServoHold();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
pressedKeysRef.current.delete(key);
|
pressedKeysRef.current.delete(key);
|
||||||
updateFromKeys();
|
|
||||||
|
if (bindingHas(keymap.cameraUp, key)) {
|
||||||
|
cameraHoldRef.current.delete('up');
|
||||||
|
updateServoHold();
|
||||||
|
} else if (bindingHas(keymap.cameraDown, key)) {
|
||||||
|
cameraHoldRef.current.delete('down');
|
||||||
|
updateServoHold();
|
||||||
|
}
|
||||||
|
|
||||||
|
driveFromKeys();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleBlur() {
|
function handleBlur() {
|
||||||
@@ -192,11 +258,18 @@ export default function KeyboardInputManager() {
|
|||||||
window.removeEventListener('keyup', handleKeyUp);
|
window.removeEventListener('keyup', handleKeyUp);
|
||||||
window.removeEventListener('blur', handleBlur);
|
window.removeEventListener('blur', handleBlur);
|
||||||
};
|
};
|
||||||
}, [keymap, nudgeServo, resetAll, runMacro, servoStep, setMode, updateFromKeys]);
|
}, [actionKeys, driveFromKeys, keymap, resetAll, runMacro, setMode, updateServoHold]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
resetAll();
|
resetAll();
|
||||||
}, [state.roverId, resetAll]);
|
}, [state.roverId, resetAll]);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => () => {
|
||||||
|
stopServoLoop();
|
||||||
|
},
|
||||||
|
[stopServoLoop],
|
||||||
|
);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user