mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
thing in HUD
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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