mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-17 10:00:46 -04:00
keyboard camerea tilt slider
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
@@ -11,7 +11,7 @@
|
|||||||
<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-BZaJy6qR.js"></script>
|
<script type="module" crossorigin src="/assets/index-DPEgmdvg.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-B88Ix9u6.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-B88Ix9u6.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -168,7 +168,6 @@ function InlineCameraTilt({ keymap }) {
|
|||||||
: (min + max) / 2;
|
: (min + max) / 2;
|
||||||
|
|
||||||
const [pendingAngle, setPendingAngle] = useState(value);
|
const [pendingAngle, setPendingAngle] = useState(value);
|
||||||
const throttleRef = useRef(null);
|
|
||||||
const draggingRef = useRef(false);
|
const draggingRef = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -177,38 +176,16 @@ function InlineCameraTilt({ keymap }) {
|
|||||||
}
|
}
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
useEffect(
|
|
||||||
() => () => {
|
|
||||||
if (throttleRef.current) {
|
|
||||||
clearTimeout(throttleRef.current);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const scheduleSend = (next) => {
|
|
||||||
if (throttleRef.current) {
|
|
||||||
clearTimeout(throttleRef.current);
|
|
||||||
}
|
|
||||||
throttleRef.current = setTimeout(() => {
|
|
||||||
setServoAngle(next);
|
|
||||||
}, 150);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSlider = (event) => {
|
const handleSlider = (event) => {
|
||||||
const next = Number.parseFloat(event.target.value);
|
const next = Number.parseFloat(event.target.value);
|
||||||
if (Number.isNaN(next)) return;
|
if (Number.isNaN(next)) return;
|
||||||
draggingRef.current = true;
|
draggingRef.current = true;
|
||||||
setPendingAngle(next);
|
setPendingAngle(next);
|
||||||
scheduleSend(next);
|
setServoAngle(next);
|
||||||
};
|
};
|
||||||
|
|
||||||
const commitSlider = () => {
|
const commitSlider = () => {
|
||||||
draggingRef.current = false;
|
draggingRef.current = false;
|
||||||
if (throttleRef.current) {
|
|
||||||
clearTimeout(throttleRef.current);
|
|
||||||
}
|
|
||||||
setServoAngle(pendingAngle);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!enabled) return null;
|
if (!enabled) return null;
|
||||||
|
|||||||
@@ -43,6 +43,37 @@ function clampSpeed(value, fallback) {
|
|||||||
return Math.max(0, Math.min(500, num));
|
return Math.max(0, Math.min(500, num));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TILT_INTERVAL_MIN = 5;
|
||||||
|
const TILT_INTERVAL_MAX = 500;
|
||||||
|
const TILT_SPEED_MIN = 1;
|
||||||
|
const TILT_SPEED_MAX = 100;
|
||||||
|
|
||||||
|
function clampTiltInterval(value, fallback) {
|
||||||
|
const num = Number(value);
|
||||||
|
if (!Number.isFinite(num)) return fallback;
|
||||||
|
return Math.max(TILT_INTERVAL_MIN, Math.min(TILT_INTERVAL_MAX, num));
|
||||||
|
}
|
||||||
|
|
||||||
|
function clampTiltSpeed(value, fallback) {
|
||||||
|
const num = Number(value);
|
||||||
|
if (!Number.isFinite(num)) return fallback;
|
||||||
|
return Math.max(TILT_SPEED_MIN, Math.min(TILT_SPEED_MAX, num));
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapTiltSpeedToInterval(speed) {
|
||||||
|
const clampedSpeed = clampTiltSpeed(speed, speed);
|
||||||
|
const ratio = (clampedSpeed - TILT_SPEED_MIN) / (TILT_SPEED_MAX - TILT_SPEED_MIN);
|
||||||
|
const interval = TILT_INTERVAL_MAX - ratio * (TILT_INTERVAL_MAX - TILT_INTERVAL_MIN);
|
||||||
|
return Math.round(interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapTiltIntervalToSpeed(interval) {
|
||||||
|
const clampedInterval = clampTiltInterval(interval, interval);
|
||||||
|
const ratio = (TILT_INTERVAL_MAX - clampedInterval) / (TILT_INTERVAL_MAX - TILT_INTERVAL_MIN);
|
||||||
|
const speed = TILT_SPEED_MIN + ratio * (TILT_SPEED_MAX - TILT_SPEED_MIN);
|
||||||
|
return Math.round(speed);
|
||||||
|
}
|
||||||
|
|
||||||
function useKeyCapture(onCapture) {
|
function useKeyCapture(onCapture) {
|
||||||
const [active, setActive] = useState(null);
|
const [active, setActive] = useState(null);
|
||||||
|
|
||||||
@@ -67,15 +98,15 @@ function useKeyCapture(onCapture) {
|
|||||||
return { active, startCapture: setActive, cancel: () => setActive(null) };
|
return { active, startCapture: setActive, cancel: () => setActive(null) };
|
||||||
}
|
}
|
||||||
|
|
||||||
function SpeedField({ label, description, value, onChange }) {
|
function SpeedField({ label, description, value, onChange, min = 0, max = 500, step = 5 }) {
|
||||||
return (
|
return (
|
||||||
<label className="surface-muted block p-0.5">
|
<label className="surface-muted block p-0.5">
|
||||||
<div className="flex items-center justify-between text-xs text-slate-300">
|
<div className="flex items-center justify-between text-xs text-slate-300">
|
||||||
<span className="font-semibold text-slate-100">{label}</span>
|
<span className="font-semibold text-slate-100">{label}</span>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min={0}
|
min={min}
|
||||||
max={500}
|
max={max}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(event) => onChange(event.target.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"
|
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"
|
||||||
@@ -85,9 +116,9 @@ function SpeedField({ label, description, value, onChange }) {
|
|||||||
<div className="mt-0.5 flex items-center gap-0.5">
|
<div className="mt-0.5 flex items-center gap-0.5">
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
min={0}
|
min={min}
|
||||||
max={500}
|
max={max}
|
||||||
step={5}
|
step={step}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(event) => onChange(event.target.value)}
|
onChange={(event) => onChange(event.target.value)}
|
||||||
className="h-2 flex-1 accent-emerald-400"
|
className="h-2 flex-1 accent-emerald-400"
|
||||||
@@ -104,7 +135,22 @@ export default function KeymapSettings() {
|
|||||||
actions: { updateKeyBinding, resetKeyBindings },
|
actions: { updateKeyBinding, resetKeyBindings },
|
||||||
} = useControlSystem();
|
} = useControlSystem();
|
||||||
const { value: inputSettings, save: saveInputSettings } = useSettingsNamespace('inputs', INPUT_SETTINGS_DEFAULTS);
|
const { value: inputSettings, save: saveInputSettings } = useSettingsNamespace('inputs', INPUT_SETTINGS_DEFAULTS);
|
||||||
const keyboardSpeeds = inputSettings.keyboard ?? INPUT_SETTINGS_DEFAULTS.keyboard;
|
const keyboardSettings = {
|
||||||
|
...INPUT_SETTINGS_DEFAULTS.keyboard,
|
||||||
|
...(inputSettings.keyboard ?? {}),
|
||||||
|
};
|
||||||
|
const tiltSpeed = useMemo(() => {
|
||||||
|
if (typeof keyboardSettings.tiltSpeed === 'number') {
|
||||||
|
return clampTiltSpeed(keyboardSettings.tiltSpeed, INPUT_SETTINGS_DEFAULTS.keyboard.tiltSpeed);
|
||||||
|
}
|
||||||
|
if (typeof keyboardSettings.tiltIntervalMs === 'number') {
|
||||||
|
return clampTiltSpeed(
|
||||||
|
mapTiltIntervalToSpeed(keyboardSettings.tiltIntervalMs),
|
||||||
|
INPUT_SETTINGS_DEFAULTS.keyboard.tiltSpeed,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return INPUT_SETTINGS_DEFAULTS.keyboard.tiltSpeed;
|
||||||
|
}, [keyboardSettings.tiltIntervalMs, keyboardSettings.tiltSpeed]);
|
||||||
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);
|
||||||
@@ -130,6 +176,23 @@ export default function KeymapSettings() {
|
|||||||
[saveInputSettings],
|
[saveInputSettings],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const updateTiltInterval = useCallback(
|
||||||
|
(nextValue) => {
|
||||||
|
const defaults = INPUT_SETTINGS_DEFAULTS.keyboard;
|
||||||
|
const clamped = clampTiltSpeed(nextValue, defaults.tiltSpeed);
|
||||||
|
const mappedInterval = mapTiltSpeedToInterval(clamped);
|
||||||
|
saveInputSettings((prev) => ({
|
||||||
|
...(prev ?? {}),
|
||||||
|
keyboard: {
|
||||||
|
...(prev?.keyboard ?? defaults),
|
||||||
|
tiltSpeed: clamped,
|
||||||
|
tiltIntervalMs: mappedInterval,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
[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">
|
||||||
@@ -147,23 +210,35 @@ export default function KeymapSettings() {
|
|||||||
<SpeedField
|
<SpeedField
|
||||||
label="Base speed"
|
label="Base speed"
|
||||||
description="Normal driving speed"
|
description="Normal driving speed"
|
||||||
value={keyboardSpeeds.baseSpeed}
|
value={keyboardSettings.baseSpeed}
|
||||||
onChange={(value) => updateKeyboardSpeed('baseSpeed', value)}
|
onChange={(value) => updateKeyboardSpeed('baseSpeed', value)}
|
||||||
/>
|
/>
|
||||||
<SpeedField
|
<SpeedField
|
||||||
label="Turbo speed"
|
label="Turbo speed"
|
||||||
description="Used when holding the boost modifier"
|
description="Used when holding the boost modifier"
|
||||||
value={keyboardSpeeds.turboSpeed}
|
value={keyboardSettings.turboSpeed}
|
||||||
onChange={(value) => updateKeyboardSpeed('turboSpeed', value)}
|
onChange={(value) => updateKeyboardSpeed('turboSpeed', value)}
|
||||||
/>
|
/>
|
||||||
<SpeedField
|
<SpeedField
|
||||||
label="Precision speed"
|
label="Precision speed"
|
||||||
description="Used when holding the precision modifier"
|
description="Used when holding the precision modifier"
|
||||||
value={keyboardSpeeds.precisionSpeed}
|
value={keyboardSettings.precisionSpeed}
|
||||||
onChange={(value) => updateKeyboardSpeed('precisionSpeed', value)}
|
onChange={(value) => updateKeyboardSpeed('precisionSpeed', value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-0.5 surface">
|
||||||
|
<p className="text-[0.7rem] text-slate-500">Camera tilt</p>
|
||||||
|
<SpeedField
|
||||||
|
label="Tilt speed"
|
||||||
|
description="Higher = faster when holding tilt keys"
|
||||||
|
value={tiltSpeed}
|
||||||
|
onChange={updateTiltInterval}
|
||||||
|
min={1}
|
||||||
|
max={100}
|
||||||
|
step={1}
|
||||||
|
/>
|
||||||
|
</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">
|
||||||
|
|||||||
@@ -14,9 +14,12 @@ import {
|
|||||||
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 NOTE_MIN = SONG_NOTE_RANGE[0];
|
const NOTE_MIN = SONG_NOTE_RANGE[0];
|
||||||
const NOTE_MAX = SONG_NOTE_RANGE[1];
|
const NOTE_MAX = SONG_NOTE_RANGE[1];
|
||||||
|
const TILT_INTERVAL_MIN = 5;
|
||||||
|
const TILT_INTERVAL_MAX = 500;
|
||||||
|
const TILT_SPEED_MIN = 1;
|
||||||
|
const TILT_SPEED_MAX = 100;
|
||||||
|
|
||||||
function clampSpeed(value, fallback) {
|
function clampSpeed(value, fallback) {
|
||||||
const num = Number(value);
|
const num = Number(value);
|
||||||
@@ -24,6 +27,32 @@ function clampSpeed(value, fallback) {
|
|||||||
return Math.max(0, Math.min(500, num));
|
return Math.max(0, Math.min(500, num));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clampTiltInterval(value, fallback) {
|
||||||
|
const num = Number(value);
|
||||||
|
if (!Number.isFinite(num)) return fallback;
|
||||||
|
return Math.max(TILT_INTERVAL_MIN, Math.min(TILT_INTERVAL_MAX, num));
|
||||||
|
}
|
||||||
|
|
||||||
|
function clampTiltSpeed(value, fallback) {
|
||||||
|
const num = Number(value);
|
||||||
|
if (!Number.isFinite(num)) return fallback;
|
||||||
|
return Math.max(TILT_SPEED_MIN, Math.min(TILT_SPEED_MAX, num));
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapTiltSpeedToInterval(speed) {
|
||||||
|
const clampedSpeed = clampTiltSpeed(speed, speed);
|
||||||
|
const ratio = (clampedSpeed - TILT_SPEED_MIN) / (TILT_SPEED_MAX - TILT_SPEED_MIN);
|
||||||
|
const interval = TILT_INTERVAL_MAX - ratio * (TILT_INTERVAL_MAX - TILT_INTERVAL_MIN);
|
||||||
|
return Math.round(interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapTiltIntervalToSpeed(interval) {
|
||||||
|
const clampedInterval = clampTiltInterval(interval, interval);
|
||||||
|
const ratio = (TILT_INTERVAL_MAX - clampedInterval) / (TILT_INTERVAL_MAX - TILT_INTERVAL_MIN);
|
||||||
|
const speed = TILT_SPEED_MIN + ratio * (TILT_SPEED_MAX - TILT_SPEED_MIN);
|
||||||
|
return Math.round(speed);
|
||||||
|
}
|
||||||
|
|
||||||
function shouldIgnoreEvent(event) {
|
function shouldIgnoreEvent(event) {
|
||||||
const target = event.target;
|
const target = event.target;
|
||||||
if (!target) return false;
|
if (!target) return false;
|
||||||
@@ -127,6 +156,19 @@ export default function KeyboardInputManager() {
|
|||||||
precisionSpeed: clampSpeed(current.precisionSpeed, defaults.precisionSpeed),
|
precisionSpeed: clampSpeed(current.precisionSpeed, defaults.precisionSpeed),
|
||||||
};
|
};
|
||||||
}, [inputSettings?.keyboard]);
|
}, [inputSettings?.keyboard]);
|
||||||
|
const servoRepeatMs = useMemo(() => {
|
||||||
|
const defaults = INPUT_SETTINGS_DEFAULTS.keyboard;
|
||||||
|
const current = inputSettings?.keyboard ?? {};
|
||||||
|
const tiltSpeed = clampTiltSpeed(
|
||||||
|
typeof current.tiltSpeed === 'number'
|
||||||
|
? current.tiltSpeed
|
||||||
|
: typeof current.tiltIntervalMs === 'number'
|
||||||
|
? mapTiltIntervalToSpeed(current.tiltIntervalMs)
|
||||||
|
: defaults.tiltSpeed,
|
||||||
|
defaults.tiltSpeed,
|
||||||
|
);
|
||||||
|
return mapTiltSpeedToInterval(tiltSpeed);
|
||||||
|
}, [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],
|
||||||
@@ -200,10 +242,10 @@ export default function KeyboardInputManager() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
nudgeServo(nextDirection * servoStep);
|
nudgeServo(nextDirection * servoStep);
|
||||||
servoIntervalRef.current = setTimeout(tick, SERVO_REPEAT_MS);
|
servoIntervalRef.current = setTimeout(tick, servoRepeatMs);
|
||||||
};
|
};
|
||||||
servoIntervalRef.current = setTimeout(tick, 0);
|
servoIntervalRef.current = setTimeout(tick, 0);
|
||||||
}, [computeServoDirection, nudgeServo, servoStep, stopServoLoop]);
|
}, [computeServoDirection, nudgeServo, servoRepeatMs, servoStep, stopServoLoop]);
|
||||||
|
|
||||||
const stopSongLoop = useCallback(() => {
|
const stopSongLoop = useCallback(() => {
|
||||||
if (songIntervalRef.current) {
|
if (songIntervalRef.current) {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ export const INPUT_SETTINGS_DEFAULTS = {
|
|||||||
baseSpeed: 250,
|
baseSpeed: 250,
|
||||||
turboSpeed: 400,
|
turboSpeed: 400,
|
||||||
precisionSpeed: 125,
|
precisionSpeed: 125,
|
||||||
|
tiltSpeed: 80,
|
||||||
|
tiltIntervalMs: 110,
|
||||||
},
|
},
|
||||||
gamepad: {
|
gamepad: {
|
||||||
driveDeadzone: 0.2,
|
driveDeadzone: 0.2,
|
||||||
|
|||||||
Reference in New Issue
Block a user