mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
mobeil controls
This commit is contained in:
@@ -65,7 +65,7 @@
|
|||||||
- [x] admin panel
|
- [x] admin panel
|
||||||
- [x] drive dock action
|
- [x] drive dock action
|
||||||
- [x] gamepad mapping settings
|
- [x] gamepad mapping settings
|
||||||
- [ ] mobile controls
|
- [x] mobile controls
|
||||||
- [ ] top down map
|
- [ ] top down map
|
||||||
- [x] video tile
|
- [x] video tile
|
||||||
- [ ] Sweep `webui` for unused or unneeded files/code with verification
|
- [ ] Sweep `webui` for unused or unneeded files/code with verification
|
||||||
@@ -78,6 +78,7 @@
|
|||||||
- admin panel
|
- admin panel
|
||||||
- drive dock action
|
- drive dock action
|
||||||
- gamepad mapping settings
|
- gamepad mapping settings
|
||||||
|
- mobile controls
|
||||||
|
|
||||||
### LARGE CHANGES
|
### LARGE CHANGES
|
||||||
- Split `webui/src/mini/MiniSummaryApp.jsx` into folderized modules under `webui/src/mini/MiniSummaryApp/` with a compatibility entrypoint preserved.
|
- Split `webui/src/mini/MiniSummaryApp.jsx` into folderized modules under `webui/src/mini/MiniSummaryApp/` with a compatibility entrypoint preserved.
|
||||||
@@ -87,6 +88,7 @@
|
|||||||
- Split `webui/src/components/AdminPanel.jsx` into `webui/src/components/AdminPanel/` and extracted monitor/health/log/LLM helper modules; updated consumers to folder entrypoint and removed external wrapper file.
|
- Split `webui/src/components/AdminPanel.jsx` into `webui/src/components/AdminPanel/` and extracted monitor/health/log/LLM helper modules; updated consumers to folder entrypoint and removed external wrapper file.
|
||||||
- Moved `DriveDockAction` to `webui/src/components/DriveDockAction/index.jsx` and updated all consumers to folder entrypoint imports.
|
- Moved `DriveDockAction` to `webui/src/components/DriveDockAction/index.jsx` and updated all consumers to folder entrypoint imports.
|
||||||
- Split `webui/src/components/GamepadMappingSettings.jsx` into `webui/src/components/GamepadMappingSettings/` with extracted constants/helpers/SliderField modules and removed the standalone component file.
|
- Split `webui/src/components/GamepadMappingSettings.jsx` into `webui/src/components/GamepadMappingSettings/` with extracted constants/helpers/SliderField modules and removed the standalone component file.
|
||||||
|
- Split `webui/src/components/MobileControls.jsx` into `webui/src/components/MobileControls/` with extracted joystick/aux/constants modules; preserved named exports and moved app import to folder entrypoint.
|
||||||
|
|
||||||
## Done criteria (per item)
|
## Done criteria (per item)
|
||||||
- [ ] Folderized structure created.
|
- [ ] Folderized structure created.
|
||||||
|
|||||||
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-BmNE-cUP.js"></script>
|
<script type="module" crossorigin src="/assets/index-CTHeAti4.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-DVTOmRBl.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-DVTOmRBl.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ import AlertFeed from './components/AlertFeed.jsx';
|
|||||||
import MobileControls, {
|
import MobileControls, {
|
||||||
MobileActionsColumn,
|
MobileActionsColumn,
|
||||||
MobileDriveColumn,
|
MobileDriveColumn,
|
||||||
} from './components/MobileControls.jsx';
|
} from './components/MobileControls/index.jsx';
|
||||||
import {
|
import {
|
||||||
ControlSystemProvider,
|
ControlSystemProvider,
|
||||||
KeyboardInputManager,
|
KeyboardInputManager,
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
// Floating on-screen joystick used for mobile driving.
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { clampUnit } from '../../controls/controlMath.js';
|
||||||
|
|
||||||
|
export default function FloatingJoystick({ disabled, layout, radius, 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), radius);
|
||||||
|
const angle = Math.atan2(dy, dx);
|
||||||
|
const knobX = Math.cos(angle) * distance;
|
||||||
|
const knobY = Math.sin(angle) * distance;
|
||||||
|
const vector = {
|
||||||
|
x: clampUnit(knobX / radius),
|
||||||
|
y: clampUnit(-knobY / radius),
|
||||||
|
boost: false,
|
||||||
|
};
|
||||||
|
setVisual((prev) => ({ ...prev, knob: { x: knobX, y: knobY } }));
|
||||||
|
onMove?.(vector);
|
||||||
|
},
|
||||||
|
[disabled, onMove, radius],
|
||||||
|
);
|
||||||
|
|
||||||
|
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 = 'h-full';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
role="presentation"
|
||||||
|
className={`relative w-full ${heightClass} select-none overflow-hidden rounded-xl border-2 border-slate-700 bg-slate-900/70 text-slate-100 shadow-md`}
|
||||||
|
style={{ touchAction: 'none' }}
|
||||||
|
onPointerDown={handlePointerDown}
|
||||||
|
onPointerMove={handlePointerMove}
|
||||||
|
onPointerUp={handlePointerEnd}
|
||||||
|
onPointerLeave={handlePointerEnd}
|
||||||
|
onPointerCancel={handlePointerEnd}
|
||||||
|
onContextMenu={(event) => event.preventDefault()}
|
||||||
|
>
|
||||||
|
{!visual.active && (
|
||||||
|
<div className="absolute inset-x-0 top-0 flex flex-col items-center gap-0 text-center pt-0.5">
|
||||||
|
<span className="font-semibold text-slate-200">Joystick area</span>
|
||||||
|
<span className="text-sm text-slate-300">Touch and hold to use the joystick</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{visual.active && (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="pointer-events-none absolute h-28 w-28 -translate-x-1/2 -translate-y-1/2 bg-cyan-400/10 outline outline-2 outline-cyan-400/60 [clip-path:circle(50%)]"
|
||||||
|
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 bg-cyan-300/80 shadow-lg [clip-path:circle(50%)]"
|
||||||
|
style={{
|
||||||
|
left: visual.base.x + visual.knob.x,
|
||||||
|
top: visual.base.y + visual.knob.y,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// Hold-to-run aux motor button for mobile controls.
|
||||||
|
export default function MobileAuxButton({ id, label, values, color, disabled, onPress, onRelease }) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={disabled}
|
||||||
|
onPointerDown={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
onPress(id, values);
|
||||||
|
}}
|
||||||
|
onPointerUp={() => onRelease(id)}
|
||||||
|
onPointerLeave={() => onRelease(id)}
|
||||||
|
onPointerCancel={() => onRelease(id)}
|
||||||
|
onContextMenu={(event) => event.preventDefault()}
|
||||||
|
className={`flex h-full w-full items-center justify-center rounded-xl border-2 px-1 py-0.75 text-center text-sm font-semibold text-white transition select-none no-touch-select ${color} hover:brightness-110 active:brightness-125 active:scale-[0.99] disabled:opacity-30`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
+19
-152
@@ -1,130 +1,21 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
// Mobile controls layout and interaction wiring.
|
||||||
import { useControlSystem } from '../controls/index.js';
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
import { clampUnit } from '../controls/controlMath.js';
|
import { useControlSystem } from '../../controls/index.js';
|
||||||
import DriveDockAction, { useDriveDockState } from './DriveDockAction/index.jsx';
|
import { clampUnit } from '../../controls/controlMath.js';
|
||||||
import NightVisionControl from './NightVisionControl.jsx';
|
import DriveDockAction, { useDriveDockState } from '../DriveDockAction/index.jsx';
|
||||||
import HornControl from './HornControl.jsx';
|
import NightVisionControl from '../NightVisionControl.jsx';
|
||||||
import CameraTiltControl from './CameraTiltControl.jsx';
|
import HornControl from '../HornControl.jsx';
|
||||||
|
import CameraTiltControl from '../CameraTiltControl.jsx';
|
||||||
const SOURCE = 'mobile-joystick';
|
import FloatingJoystick from './FloatingJoystick.jsx';
|
||||||
const JOYSTICK_RADIUS = 80;
|
import MobileAuxButton from './MobileAuxButton.jsx';
|
||||||
const JOYSTICK_SMOOTHING = 0.15;
|
import {
|
||||||
const AUX_ZERO = { main: 0, side: 0, vacuum: 0 };
|
SOURCE,
|
||||||
const AUX_ALL_FORWARD = { main: 127, side: 127, vacuum: 127 };
|
JOYSTICK_RADIUS,
|
||||||
const AUX_ALL_BACKWARD = { main: -127, side: -127, vacuum: -127 };
|
JOYSTICK_SMOOTHING,
|
||||||
|
AUX_ZERO,
|
||||||
function FloatingJoystick({ disabled, layout, radius, onMove, onStop }) {
|
AUX_ALL_FORWARD,
|
||||||
const containerRef = useRef(null);
|
AUX_ALL_BACKWARD,
|
||||||
const pointerIdRef = useRef(null);
|
} from './constants.js';
|
||||||
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), radius);
|
|
||||||
const angle = Math.atan2(dy, dx);
|
|
||||||
const knobX = Math.cos(angle) * distance;
|
|
||||||
const knobY = Math.sin(angle) * distance;
|
|
||||||
const vector = {
|
|
||||||
x: clampUnit(knobX / radius),
|
|
||||||
y: clampUnit(-knobY / 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 = 'h-full';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={containerRef}
|
|
||||||
role="presentation"
|
|
||||||
className={`relative w-full ${heightClass} select-none overflow-hidden rounded-xl border-2 border-slate-700 bg-slate-900/70 text-slate-100 shadow-md`}
|
|
||||||
style={{ touchAction: 'none' }}
|
|
||||||
onPointerDown={handlePointerDown}
|
|
||||||
onPointerMove={handlePointerMove}
|
|
||||||
onPointerUp={handlePointerEnd}
|
|
||||||
onPointerLeave={handlePointerEnd}
|
|
||||||
onPointerCancel={handlePointerEnd}
|
|
||||||
onContextMenu={(event) => event.preventDefault()}
|
|
||||||
>
|
|
||||||
{!visual.active && (
|
|
||||||
<div className="absolute inset-x-0 top-0 flex flex-col items-center gap-0 text-center pt-0.5">
|
|
||||||
<span className="font-semibold text-slate-200">Joystick area</span>
|
|
||||||
<span className="text-sm text-slate-300">Touch and hold to use the joystick</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{visual.active && (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className="pointer-events-none absolute h-28 w-28 -translate-x-1/2 -translate-y-1/2 bg-cyan-400/10 outline outline-2 outline-cyan-400/60 [clip-path:circle(50%)]"
|
|
||||||
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 bg-cyan-300/80 shadow-lg [clip-path:circle(50%)]"
|
|
||||||
style={{
|
|
||||||
left: visual.base.x + visual.knob.x,
|
|
||||||
top: visual.base.y + visual.knob.y,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MobileJoystickPanel({ layout }) {
|
function MobileJoystickPanel({ layout }) {
|
||||||
const {
|
const {
|
||||||
@@ -140,9 +31,7 @@ function MobileJoystickPanel({ layout }) {
|
|||||||
const smoothedVectorRef = useRef({ x: 0, y: 0, boost: false });
|
const smoothedVectorRef = useRef({ x: 0, y: 0, boost: false });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (disabled) {
|
if (disabled) smoothedVectorRef.current = { x: 0, y: 0, boost: false };
|
||||||
smoothedVectorRef.current = { x: 0, y: 0, boost: false };
|
|
||||||
}
|
|
||||||
}, [disabled]);
|
}, [disabled]);
|
||||||
|
|
||||||
const handleMove = useCallback(
|
const handleMove = useCallback(
|
||||||
@@ -198,32 +87,10 @@ function MobileJoystickPanel({ layout }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{/* Panic stop button can be re-enabled here if needed */}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MobileAuxButton({ id, label, values, color, disabled, onPress, onRelease }) {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
disabled={disabled}
|
|
||||||
onPointerDown={(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
onPress(id, values);
|
|
||||||
}}
|
|
||||||
onPointerUp={() => onRelease(id)}
|
|
||||||
onPointerLeave={() => onRelease(id)}
|
|
||||||
onPointerCancel={() => onRelease(id)}
|
|
||||||
onContextMenu={(event) => event.preventDefault()}
|
|
||||||
className={`flex h-full w-full items-center justify-center rounded-xl border-2 px-1 py-0.75 text-center text-sm font-semibold text-white transition select-none no-touch-select ${color} hover:brightness-110 active:brightness-125 active:scale-[0.99] disabled:opacity-30`}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MobileActionsColumnContent({ layout }) {
|
function MobileActionsColumnContent({ layout }) {
|
||||||
const {
|
const {
|
||||||
state: { roverId, camera, horn },
|
state: { roverId, camera, horn },
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// Mobile controls constants.
|
||||||
|
export const SOURCE = 'mobile-joystick';
|
||||||
|
export const JOYSTICK_RADIUS = 80;
|
||||||
|
export const JOYSTICK_SMOOTHING = 0.15;
|
||||||
|
export const AUX_ZERO = { main: 0, side: 0, vacuum: 0 };
|
||||||
|
export const AUX_ALL_FORWARD = { main: 127, side: 127, vacuum: 127 };
|
||||||
|
export const AUX_ALL_BACKWARD = { main: -127, side: -127, vacuum: -127 };
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import MobilePortraitControls, { MobileActionsColumn, MobileDriveColumn } from './MobileControlsContent.jsx';
|
||||||
|
|
||||||
|
export { MobileActionsColumn, MobileDriveColumn };
|
||||||
|
export default MobilePortraitControls;
|
||||||
Reference in New Issue
Block a user