mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
thing in HUD
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-bOy4wWKD.js"></script>
|
<script type="module" crossorigin src="/assets/index-C6EqAuKU.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-BJwrnf9g.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-DkBejE8J.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import KeymapSettings from './KeymapSettings.jsx';
|
|||||||
import GamepadMappingSettings from './GamepadMappingSettings.jsx';
|
import GamepadMappingSettings from './GamepadMappingSettings.jsx';
|
||||||
import Tabs, { Tab, TabList, TabPanel, TabPanels } from './Tabs.jsx';
|
import Tabs, { Tab, TabList, TabPanel, TabPanels } from './Tabs.jsx';
|
||||||
import SessionSnapshot from './SessionSnapshot.jsx';
|
import SessionSnapshot from './SessionSnapshot.jsx';
|
||||||
|
import { useHudMapSetting } from '../hooks/useHudMapSetting.js';
|
||||||
|
|
||||||
const manualTabs = [
|
const manualTabs = [
|
||||||
{ key: 'start', label: 'Start OI' },
|
{ key: 'start', label: 'Start OI' },
|
||||||
@@ -21,6 +22,7 @@ export default function SettingsPanel() {
|
|||||||
actions: { sendOiCommand, setSensorStream },
|
actions: { sendOiCommand, setSensorStream },
|
||||||
} = useControlSystem();
|
} = useControlSystem();
|
||||||
const canControl = Boolean(roverId);
|
const canControl = Boolean(roverId);
|
||||||
|
const [hudMapDesktop, setHudMapDesktop] = useHudMapSetting();
|
||||||
|
|
||||||
const sensorButtons = useMemo(
|
const sensorButtons = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@@ -39,6 +41,7 @@ export default function SettingsPanel() {
|
|||||||
<TabList>
|
<TabList>
|
||||||
<Tab id="keybindings">Keybindings</Tab>
|
<Tab id="keybindings">Keybindings</Tab>
|
||||||
<Tab id="controller">Controller</Tab>
|
<Tab id="controller">Controller</Tab>
|
||||||
|
<Tab id="page">Page settings</Tab>
|
||||||
<Tab id="admin">Admin</Tab>
|
<Tab id="admin">Admin</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanels>
|
<TabPanels>
|
||||||
@@ -52,6 +55,23 @@ export default function SettingsPanel() {
|
|||||||
<GamepadMappingSettings />
|
<GamepadMappingSettings />
|
||||||
</div>
|
</div>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
<TabPanel id="page">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<section className="panel-section space-y-0.5 text-sm">
|
||||||
|
<p className="text-slate-400">HUD</p>
|
||||||
|
<label className="flex items-center gap-0.5 text-slate-200">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="accent-emerald-500"
|
||||||
|
checked={hudMapDesktop}
|
||||||
|
onChange={(e) => setHudMapDesktop(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<span>Show top-down map in HUD (desktop)</span>
|
||||||
|
</label>
|
||||||
|
<p className="text-xs text-slate-500">Mobile HUD keeps the map on by default.</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</TabPanel>
|
||||||
<TabPanel id="admin">
|
<TabPanel id="admin">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<section className="panel-section space-y-0.5 text-sm">
|
<section className="panel-section space-y-0.5 text-sm">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
function TopDownMap({ sensors = {}, variant = 'full', size: overrideSize }) {
|
function TopDownMap({ sensors = {}, variant = 'full', size: overrideSize, overlay = false }) {
|
||||||
const size = overrideSize || (variant === 'mini' ? 190 : 260);
|
const size = overrideSize || (variant === 'mini' ? 190 : 260);
|
||||||
const center = size / 2;
|
const center = size / 2;
|
||||||
const innerCircle = center * 0.8; // keep proportions consistent as size changes
|
const innerCircle = center * 0.8; // keep proportions consistent as size changes
|
||||||
@@ -50,9 +50,16 @@ function TopDownMap({ sensors = {}, variant = 'full', size: overrideSize }) {
|
|||||||
const maxLight = lightMaxSamples.length ? Math.max(...lightMaxSamples, 1200) : 1200;
|
const maxLight = lightMaxSamples.length ? Math.max(...lightMaxSamples, 1200) : 1200;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="surface relative p-1" style={{ height: '100%', width: '100%', aspectRatio: '1 / 1' }}>
|
<div
|
||||||
<div className="absolute left-1 top-1 text-xs text-slate-400">Top-down</div>
|
className={`${overlay ? 'relative' : 'surface relative p-1'}`}
|
||||||
<div className="absolute right-1 top-1 text-[0.65rem] text-slate-500">0–{maxLight}</div>
|
style={overlay ? { width: `${size}px`, height: `${size}px` } : { height: '100%', width: '100%', aspectRatio: '1 / 1' }}
|
||||||
|
>
|
||||||
|
{!overlay ? (
|
||||||
|
<>
|
||||||
|
<div className="absolute left-1 top-1 text-xs text-slate-400">Top-down</div>
|
||||||
|
<div className="absolute right-1 top-1 text-[0.65rem] text-slate-500">0–{maxLight}</div>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
<svg width="100%" height="100%" viewBox={`0 0 ${size} ${size}`} preserveAspectRatio="xMidYMid meet" className="mx-auto block">
|
<svg width="100%" height="100%" viewBox={`0 0 ${size} ${size}`} preserveAspectRatio="xMidYMid meet" className="mx-auto block">
|
||||||
<circle cx={center} cy={center} r={innerCircle} fill="#0f172a" stroke="#334155" strokeWidth="2" />
|
<circle cx={center} cy={center} r={innerCircle} fill="#0f172a" stroke="#334155" strokeWidth="2" />
|
||||||
<WheelVisual
|
<WheelVisual
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { WhepPlayer } from '../lib/whepPlayer.js';
|
import { WhepPlayer } from '../lib/whepPlayer.js';
|
||||||
|
import TopDownMap from './TopDownMap.jsx';
|
||||||
|
import { useHudMapSetting } from '../hooks/useHudMapSetting.js';
|
||||||
|
|
||||||
const RESTART_DELAY_MS = 2000;
|
const RESTART_DELAY_MS = 2000;
|
||||||
const UNMUTE_RETRY_MS = 3000;
|
const UNMUTE_RETRY_MS = 3000;
|
||||||
@@ -66,6 +68,9 @@ export default function VideoTile({
|
|||||||
const sensors = telemetryFrame?.sensors;
|
const sensors = telemetryFrame?.sensors;
|
||||||
const batteryCharge = sensors?.batteryChargeMah ?? null;
|
const batteryCharge = sensors?.batteryChargeMah ?? null;
|
||||||
const desktopLayout = layoutFormat === 'desktop';
|
const desktopLayout = layoutFormat === 'desktop';
|
||||||
|
const mobileHud = !desktopLayout;
|
||||||
|
const [showHudMapDesktop, setShowHudMapDesktop] = useHudMapSetting();
|
||||||
|
const showHudMap = mobileHud ? true : showHudMapDesktop;
|
||||||
const batteryVisual = buildBatteryVisual(batteryCharge, batteryConfig);
|
const batteryVisual = buildBatteryVisual(batteryCharge, batteryConfig);
|
||||||
// console.log('[BatteryBarDebug]', {
|
// console.log('[BatteryBarDebug]', {
|
||||||
// frameSensors: sensors,
|
// frameSensors: sensors,
|
||||||
@@ -268,6 +273,7 @@ export default function VideoTile({
|
|||||||
<audio ref={audioRef} autoPlay hidden />
|
<audio ref={audioRef} autoPlay hidden />
|
||||||
<HudOverlay
|
<HudOverlay
|
||||||
frame={telemetryFrame}
|
frame={telemetryFrame}
|
||||||
|
sensors={sensors}
|
||||||
label={label}
|
label={label}
|
||||||
status={renderedStatus}
|
status={renderedStatus}
|
||||||
audioStatus={renderedAudioStatus}
|
audioStatus={renderedAudioStatus}
|
||||||
@@ -276,6 +282,8 @@ export default function VideoTile({
|
|||||||
driverLabel={driverLabel}
|
driverLabel={driverLabel}
|
||||||
battery={batteryVisual}
|
battery={batteryVisual}
|
||||||
songNote={songNote}
|
songNote={songNote}
|
||||||
|
showTopDown={showHudMap}
|
||||||
|
mobileHud={mobileHud}
|
||||||
/>
|
/>
|
||||||
<OvercurrentOverlay motors={overcurrentMotors} />
|
<OvercurrentOverlay motors={overcurrentMotors} />
|
||||||
<LowBatteryOverlay charge={batteryCharge} config={batteryConfig} />
|
<LowBatteryOverlay charge={batteryCharge} config={batteryConfig} />
|
||||||
@@ -329,6 +337,7 @@ function BatteryBarVertical({ visual }) {
|
|||||||
|
|
||||||
function HudOverlay({
|
function HudOverlay({
|
||||||
frame,
|
frame,
|
||||||
|
sensors,
|
||||||
label,
|
label,
|
||||||
status,
|
status,
|
||||||
audioStatus,
|
audioStatus,
|
||||||
@@ -337,8 +346,9 @@ function HudOverlay({
|
|||||||
driverLabel = null,
|
driverLabel = null,
|
||||||
battery,
|
battery,
|
||||||
songNote = null,
|
songNote = null,
|
||||||
|
showTopDown = false,
|
||||||
|
mobileHud = false,
|
||||||
}) {
|
}) {
|
||||||
const sensors = frame?.sensors;
|
|
||||||
const bumps = sensors?.bumpsAndWheelDrops || {};
|
const bumps = sensors?.bumpsAndWheelDrops || {};
|
||||||
const [now, setNow] = useState(() => Date.now());
|
const [now, setNow] = useState(() => Date.now());
|
||||||
|
|
||||||
@@ -383,12 +393,20 @@ function HudOverlay({
|
|||||||
{driverLabel ? <span className="text-slate-300">• {driverLabel}</span> : null}
|
{driverLabel ? <span className="text-slate-300">• {driverLabel}</span> : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="absolute top-0.5 left-1/2 flex -translate-x-1/2 gap-1 bg-black/70 text-[0.6rem] font-medium text-slate-200">
|
{showTopDown ? (
|
||||||
<div className={`${desktopLayout ? 'px-1 py-0.5' : 'px-0.5 py-0 text-nowrap'} ${bumps.bumpLeft ? 'bg-red-600 text-white animate-pulse' : 'text-slate-500'}`}>Left bump</div>
|
<div
|
||||||
<div className={`${desktopLayout ? 'px-1 py-0.5' : 'px-0.5 py-0 text-nowrap'} ${bumps.wheelDropLeft ? 'bg-red-600 text-white animate-pulse' : 'text-slate-500'}`}>Left wheel drop</div>
|
className="pointer-events-none absolute right-1 top-1"
|
||||||
<div className={`${desktopLayout ? 'px-1 py-0.5' : 'px-0.5 py-0 text-nowrap'} ${bumps.wheelDropRight ? 'bg-red-600 text-white animate-pulse' : 'text-slate-500'}`}>Right wheel drop</div>
|
style={{
|
||||||
<div className={`${desktopLayout ? 'px-1 py-0.5' : 'px-0.5 py-0 text-nowrap'} ${bumps.bumpRight ? 'bg-red-600 text-white animate-pulse' : 'text-slate-500'}`}>Right bump</div>
|
width: '240px',
|
||||||
|
height: '240px',
|
||||||
|
opacity: 0.7,
|
||||||
|
transform: `scale(${mobileHud ? 0.55 : 0.7})`,
|
||||||
|
transformOrigin: 'top right',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TopDownMap sensors={sensors} size={240} overlay />
|
||||||
</div>
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -400,26 +418,32 @@ function HudOverlay({
|
|||||||
{audioStatus ? <div>Audio: {audioStatus}</div> : null}
|
{audioStatus ? <div>Audio: {audioStatus}</div> : null}
|
||||||
</div>
|
</div>
|
||||||
{songNote != null ? (
|
{songNote != null ? (
|
||||||
<div className="absolute right-1 top-1 rounded bg-black/70 px-1 py-0.25 text-[0.65rem] font-semibold text-emerald-200">
|
<div className="absolute bottom-1 right-1 rounded bg-black/70 px-1 py-0.25 text-[0.65rem] font-semibold text-emerald-200">
|
||||||
Song {formatNoteLabel(songNote)} <span className="text-slate-400">({songNote})</span>
|
Song {formatNoteLabel(songNote)} <span className="text-slate-400">({songNote})</span>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="absolute bottom-0.5 left-1/2 flex -translate-x-1/2 gap-0.5 bg-black/80 px-0.5 py-0.5 text-slate-100">
|
<div className="absolute bottom-0.5 left-1/2 flex -translate-x-1/2 gap-0.5 bg-black/80 px-0.5 py-0.5 text-slate-100">
|
||||||
<span>Rover: "{label || 'Unnamed Rover'}"</span>
|
<span>Rover: "{label || 'Unnamed Rover'}"</span>
|
||||||
{/* <span>{pulse ? 'Sensors active' : 'No recent sensors'}</span> */}
|
{/* <span>{pulse ? 'Sensors active' : 'No recent sensors'}</span> */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{showTopDown ? (
|
||||||
{/* bump and wheel drops bar */}
|
<div
|
||||||
<div className="absolute top-0.5 left-1/2 flex -translate-x-1/2 gap-1 bg-black/70 text-[0.6rem] font-medium text-slate-200">
|
className="pointer-events-none absolute right-1 top-1"
|
||||||
<div className={`${desktopLayout ? 'px-1 py-0.5':'px-0.5 py-0 text-nowrap'} ${bumps.bumpLeft ? 'bg-red-600 text-white animate-pulse' : 'text-slate-500'}`}>Left bump</div>
|
style={{
|
||||||
<div className={`${desktopLayout ? 'px-1 py-0.5':'px-0.5 py-0 text-nowrap'} ${bumps.wheelDropLeft ? 'bg-red-600 text-white animate-pulse' : 'text-slate-500'}`}>Left wheel drop</div>
|
width: '240px',
|
||||||
<div className={`${desktopLayout ? 'px-1 py-0.5':'px-0.5 py-0 text-nowrap'} ${bumps.wheelDropRight ? 'bg-red-600 text-white animate-pulse' : 'text-slate-500'}`}>Right wheel drop</div>
|
height: '240px',
|
||||||
<div className={`${desktopLayout ? 'px-1 py-0.5':'px-0.5 py-0 text-nowrap'} ${bumps.bumpRight ? 'bg-red-600 text-white animate-pulse' : 'text-slate-500'}`}>Right bump</div>
|
opacity: 0.7,
|
||||||
|
transform: `scale(${mobileHud ? 0.55 : 0.7})`,
|
||||||
|
transformOrigin: 'top right',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TopDownMap sensors={sensors} size={240} overlay />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const OVERCURRENT_LABELS = {
|
const OVERCURRENT_LABELS = {
|
||||||
leftWheel: 'Left wheel',
|
leftWheel: 'Left wheel',
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export function useHudMapSetting() {
|
||||||
|
const [enabled, setEnabled] = useState(() => {
|
||||||
|
if (typeof window === 'undefined') return false;
|
||||||
|
return window.localStorage.getItem('hudMapDesktop') === '1';
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
window.localStorage.setItem('hudMapDesktop', enabled ? '1' : '0');
|
||||||
|
const event = new CustomEvent('hudMapDesktopChanged', { detail: enabled });
|
||||||
|
window.dispatchEvent(event);
|
||||||
|
}, [enabled]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === 'undefined') return undefined;
|
||||||
|
const handler = (event) => {
|
||||||
|
if (typeof event.detail === 'boolean') {
|
||||||
|
setEnabled(event.detail);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('hudMapDesktopChanged', handler);
|
||||||
|
return () => window.removeEventListener('hudMapDesktopChanged', handler);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return [enabled, setEnabled];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user