thing in HUD

This commit is contained in:
legop3
2025-12-21 15:42:01 -05:00
parent 8e01dcaabd
commit 1b013ed91b
9 changed files with 116 additions and 37 deletions
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
+2 -2
View File
@@ -11,8 +11,8 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
<title>Multi Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-bOy4wWKD.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BJwrnf9g.css">
<script type="module" crossorigin src="/assets/index-C6EqAuKU.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DkBejE8J.css">
</head>
<body>
<div id="root"></div>
+20
View File
@@ -6,6 +6,7 @@ import KeymapSettings from './KeymapSettings.jsx';
import GamepadMappingSettings from './GamepadMappingSettings.jsx';
import Tabs, { Tab, TabList, TabPanel, TabPanels } from './Tabs.jsx';
import SessionSnapshot from './SessionSnapshot.jsx';
import { useHudMapSetting } from '../hooks/useHudMapSetting.js';
const manualTabs = [
{ key: 'start', label: 'Start OI' },
@@ -21,6 +22,7 @@ export default function SettingsPanel() {
actions: { sendOiCommand, setSensorStream },
} = useControlSystem();
const canControl = Boolean(roverId);
const [hudMapDesktop, setHudMapDesktop] = useHudMapSetting();
const sensorButtons = useMemo(
() => [
@@ -39,6 +41,7 @@ export default function SettingsPanel() {
<TabList>
<Tab id="keybindings">Keybindings</Tab>
<Tab id="controller">Controller</Tab>
<Tab id="page">Page settings</Tab>
<Tab id="admin">Admin</Tab>
</TabList>
<TabPanels>
@@ -52,6 +55,23 @@ export default function SettingsPanel() {
<GamepadMappingSettings />
</div>
</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">
<div className="space-y-0.5">
<section className="panel-section space-y-0.5 text-sm">
+11 -4
View File
@@ -1,6 +1,6 @@
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 center = size / 2;
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;
return (
<div className="surface relative p-1" style={{ height: '100%', width: '100%', aspectRatio: '1 / 1' }}>
<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>
<div
className={`${overlay ? 'relative' : 'surface relative p-1'}`}
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">
<circle cx={center} cy={center} r={innerCircle} fill="#0f172a" stroke="#334155" strokeWidth="2" />
<WheelVisual
+42 -18
View File
@@ -1,5 +1,7 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { WhepPlayer } from '../lib/whepPlayer.js';
import TopDownMap from './TopDownMap.jsx';
import { useHudMapSetting } from '../hooks/useHudMapSetting.js';
const RESTART_DELAY_MS = 2000;
const UNMUTE_RETRY_MS = 3000;
@@ -66,6 +68,9 @@ export default function VideoTile({
const sensors = telemetryFrame?.sensors;
const batteryCharge = sensors?.batteryChargeMah ?? null;
const desktopLayout = layoutFormat === 'desktop';
const mobileHud = !desktopLayout;
const [showHudMapDesktop, setShowHudMapDesktop] = useHudMapSetting();
const showHudMap = mobileHud ? true : showHudMapDesktop;
const batteryVisual = buildBatteryVisual(batteryCharge, batteryConfig);
// console.log('[BatteryBarDebug]', {
// frameSensors: sensors,
@@ -268,6 +273,7 @@ export default function VideoTile({
<audio ref={audioRef} autoPlay hidden />
<HudOverlay
frame={telemetryFrame}
sensors={sensors}
label={label}
status={renderedStatus}
audioStatus={renderedAudioStatus}
@@ -276,6 +282,8 @@ export default function VideoTile({
driverLabel={driverLabel}
battery={batteryVisual}
songNote={songNote}
showTopDown={showHudMap}
mobileHud={mobileHud}
/>
<OvercurrentOverlay motors={overcurrentMotors} />
<LowBatteryOverlay charge={batteryCharge} config={batteryConfig} />
@@ -329,6 +337,7 @@ function BatteryBarVertical({ visual }) {
function HudOverlay({
frame,
sensors,
label,
status,
audioStatus,
@@ -337,8 +346,9 @@ function HudOverlay({
driverLabel = null,
battery,
songNote = null,
showTopDown = false,
mobileHud = false,
}) {
const sensors = frame?.sensors;
const bumps = sensors?.bumpsAndWheelDrops || {};
const [now, setNow] = useState(() => Date.now());
@@ -383,12 +393,20 @@ function HudOverlay({
{driverLabel ? <span className="text-slate-300"> {driverLabel}</span> : null}
</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">
<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 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>
<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>
<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>
{showTopDown ? (
<div
className="pointer-events-none absolute right-1 top-1"
style={{
width: '240px',
height: '240px',
opacity: 0.7,
transform: `scale(${mobileHud ? 0.55 : 0.7})`,
transformOrigin: 'top right',
}}
>
<TopDownMap sensors={sensors} size={240} overlay />
</div>
) : null}
</div>
);
}
@@ -400,26 +418,32 @@ function HudOverlay({
{audioStatus ? <div>Audio: {audioStatus}</div> : null}
</div>
{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>
</div>
) : 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">
<span>Rover: "{label || 'Unnamed Rover'}"</span>
{/* <span>{pulse ? 'Sensors active' : 'No recent sensors'}</span> */}
</div>
</div>
{/* bump and wheel drops bar */}
<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">
<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 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>
<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>
<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>
{showTopDown ? (
<div
className="pointer-events-none absolute right-1 top-1"
style={{
width: '240px',
height: '240px',
opacity: 0.7,
transform: `scale(${mobileHud ? 0.55 : 0.7})`,
transformOrigin: 'top right',
}}
>
<TopDownMap sensors={sensors} size={240} overlay />
</div>
) : null}
</div>
</div>
);
}
);
}
const OVERCURRENT_LABELS = {
leftWheel: 'Left wheel',
+28
View File
@@ -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];
}