hud stuff

This commit is contained in:
legop3
2026-05-02 17:00:06 -04:00
parent ba4b6cfc78
commit 2f570d92d7
8 changed files with 183 additions and 162 deletions
+4 -1
View File
@@ -1,6 +1,7 @@
// Battery Bar
// Purpose: Defines the Battery Bar module and the local helpers/components used in this file.
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import React from 'react';
import { WARN_DISPLAY_PERCENT } from '../../lib/battery.js';
const WARN_FLASH_MS = 1600;
@@ -10,7 +11,7 @@ function classNames(...values) {
return values.filter(Boolean).join(' ');
}
export default function BatteryBar({
function BatteryBar({
visual,
orientation = 'horizontal',
variant = 'inline',
@@ -155,3 +156,5 @@ export default function BatteryBar({
</div>
);
}
export default React.memo(BatteryBar);
@@ -1,23 +1,24 @@
// Hud Chat Input
// Purpose: Defines the Hud Chat Input module and the local helpers/components used in this file.
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import { useMemo, useState } from 'react';
import { memo, useMemo, useState } from 'react';
import { useChat } from '../../context/ChatContext.jsx';
import { useSessionSelector } from '../../context/SessionContext.jsx';
import { useSettingsNamespace } from '../../settings/index.js';
export default function HudChatInput({ compact = false }) {
const session = useSessionSelector((state) => state.session);
function HudChatInput({ compact = false }) {
const role = useSessionSelector((state) => state.session?.role || null);
const currentRoverId = useSessionSelector((state) => state.session?.assignment?.roverId || null);
const roverRoster = useSessionSelector((state) => state.session?.roster || []);
const { sendMessage, onInputFocus, onInputBlur, blurChat, registerInputRef, setTypingActive } = useChat();
const { value: ttsSettings } = useSettingsNamespace('tts', { engine: 'flite', voice: 'rms', pitch: 50 });
const [draft, setDraft] = useState('');
const [sending, setSending] = useState(false);
const canChat = session?.role !== 'spectator';
const hideHudChat = session?.role === 'spectator';
const currentRoverId = session?.assignment?.roverId || null;
const canChat = role !== 'spectator';
const hideHudChat = role === 'spectator';
const rover = useMemo(
() => session?.roster?.find((entry) => String(entry.id) === String(currentRoverId)) || null,
[currentRoverId, session?.roster],
() => roverRoster.find((entry) => String(entry.id) === String(currentRoverId)) || null,
[currentRoverId, roverRoster],
);
const ttsSupported = Boolean(rover?.audio?.ttsEnabled);
const ttsPayload = useMemo(() => {
@@ -98,3 +99,5 @@ export default function HudChatInput({ compact = false }) {
</form>
);
}
export default memo(HudChatInput);
@@ -3,7 +3,7 @@
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import React from 'react';
export default function LowBatteryOverlay({ battery, compact = false }) {
function LowBatteryOverlay({ battery, compact = false }) {
if (!battery?.available) return null;
if (!battery.warnActive && !battery.urgentActive) return null;
@@ -24,3 +24,5 @@ export default function LowBatteryOverlay({ battery, compact = false }) {
</div>
);
}
export default React.memo(LowBatteryOverlay);
@@ -4,7 +4,7 @@
import React from 'react';
import { OVERCURRENT_LABELS } from './constants.js';
export default function OvercurrentOverlay({ motors, fill = 0, compact = false }) {
function OvercurrentOverlay({ motors, fill = 0, compact = false }) {
if (!motors?.length) return null;
const safeLabels = motors.map((name) => OVERCURRENT_LABELS[name] || name);
const containerClass = compact ? 'w-[12rem] h-[3.5rem]' : 'w-[20rem] h-[7rem]';
@@ -29,3 +29,5 @@ export default function OvercurrentOverlay({ motors, fill = 0, compact = false }
</div>
);
}
export default React.memo(OvercurrentOverlay);
+35 -24
View File
@@ -48,7 +48,15 @@ export default function VideoTile({
isActiveDriver = false,
idleSkipSeconds = null,
}) {
const session = useSessionSelector((state) => state.session);
const discordUrl = useSessionSelector((state) => {
const socials = state.session?.socials || [];
const socialUrl =
socials.find((entry) => {
const key = String(entry?.id || entry?.label || '').toLowerCase();
return key === 'discord';
})?.url || null;
return socialUrl || state.session?.discord?.invite || null;
});
const videoRef = useRef(null);
const audioRef = useRef(null);
const restartTimer = useRef(null);
@@ -92,12 +100,15 @@ export default function VideoTile({
const showHudMap = hudForceMap ? true : mobileHud ? true : showHudMapDesktop;
const batteryVisual = buildBatteryVisual({ charge: batteryCharge, config: batteryConfig });
const wheelOvercurrents = sensors?.wheelOvercurrents || null;
const overcurrentMotors =
wheelOvercurrents == null
? []
: Object.entries(wheelOvercurrents)
.filter(([, active]) => Boolean(active))
.map(([key]) => key);
const overcurrentMotors = useMemo(
() =>
wheelOvercurrents == null
? []
: Object.entries(wheelOvercurrents)
.filter(([, active]) => Boolean(active))
.map(([key]) => key),
[wheelOvercurrents],
);
const limiterCaps = overcurrentLimiter?.caps || null;
const limiterGroups = overcurrentLimiter?.overcurrent?.groups || null;
const debugFlags = useMemo(() => {
@@ -119,9 +130,15 @@ export default function VideoTile({
return Math.max(0, Math.min(1, 1 - Math.min(driveCap, auxCap)));
}, [limiterCaps]);
const limiterActive = Boolean(overcurrentLimiter?.isActive);
const overlayMotors = overcurrentMotors.length ? overcurrentMotors : limiterActive ? ['limiter'] : [];
const overlayFill = limiterFill ?? (overcurrentMotors.length ? 1 : 0);
const overlayVisible = Boolean(overlayMotors.length);
const overlayState = useMemo(() => {
const motors = overcurrentMotors.length ? overcurrentMotors : limiterActive ? ['limiter'] : [];
const fill = limiterFill ?? (overcurrentMotors.length ? 1 : 0);
return {
motors,
fill,
visible: Boolean(motors.length),
};
}, [overcurrentMotors, limiterActive, limiterFill]);
const mainBrushActive = Boolean(
(Number(sensors?.mainBrushCurrentMa) || 0) > BRUSH_CURRENT_THRESHOLD_MA ||
sensors?.wheelOvercurrents?.mainBrush,
@@ -196,26 +213,18 @@ export default function VideoTile({
},
[debugAudio, label],
);
const discordUrl =
session?.socials?.find((entry) => {
const key = String(entry?.id || entry?.label || '').toLowerCase();
return key === 'discord';
})?.url ||
session?.discord?.invite ||
null;
useEffect(() => {
if (!debugHud) return;
console.log('[OvercurrentHUD]', {
overlayVisible,
overlayMotors,
overlayFill,
overlayVisible: overlayState.visible,
overlayMotors: overlayState.motors,
overlayFill: overlayState.fill,
limiterActive,
limiterCaps,
limiterGroups,
wheelOvercurrents,
});
}, [debugHud, overlayFill, overlayMotors, overlayVisible, limiterActive, limiterCaps, limiterGroups, wheelOvercurrents]);
}, [debugHud, overlayState, limiterActive, limiterCaps, limiterGroups, wheelOvercurrents]);
useEffect(() => {
logAudio('settings/update');
@@ -631,10 +640,12 @@ export default function VideoTile({
{!noHud ? <HudChatInput compact={mobileHud} /> : null}
{!noHud && debugHud ? (
<div className="pointer-events-none absolute left-1 top-1 z-40 rounded bg-black/80 px-1 py-0.5 text-[0.6rem] text-lime-200">
{`OC vis:${overlayVisible ? 1 : 0} motors:${overlayMotors.length} fill:${Math.round(overlayFill * 100)}%`}
{`OC vis:${overlayState.visible ? 1 : 0} motors:${overlayState.motors.length} fill:${Math.round(
overlayState.fill * 100,
)}%`}
</div>
) : null}
{!noHud ? <OvercurrentOverlay motors={overlayMotors} fill={overlayFill} compact={mobileHud} /> : null}
{!noHud ? <OvercurrentOverlay motors={overlayState.motors} fill={overlayState.fill} compact={mobileHud} /> : null}
{!noHud ? <LowBatteryOverlay battery={batteryVisual} compact={mobileHud} /> : null}
{!noHud && showVerticalBattery && batteryVisual.available ? (
<div className="pointer-events-none absolute right-1 top-1/2 flex h-[70%] -translate-y-1/2 flex-col items-center justify-center rounded bg-black/60 px-0.5 pb-1 pt-1">