export interface NormalizedHudPosition { readonly x: number; readonly y: number; } export interface HudSize { readonly width: number; readonly height: number; } export const DEFAULT_THREAT_METER_POSITION: NormalizedHudPosition = Object.freeze({ x: 0.78, y: 0.18, }); function finite(value: number, fallback: number): number { return Number.isFinite(value) ? value : fallback; } function clamp(value: number, minimum: number, maximum: number): number { return Math.max(minimum, Math.min(maximum, value)); } export function normalizeHudPosition( value: unknown, fallback: NormalizedHudPosition = DEFAULT_THREAT_METER_POSITION, ): NormalizedHudPosition { if (!value || typeof value !== "object") return fallback; const candidate = value as Partial; return { x: clamp(finite(candidate.x ?? Number.NaN, fallback.x), 0, 1), y: clamp(finite(candidate.y ?? Number.NaN, fallback.y), 0, 1), }; } /** * Keeps a center-anchored HUD panel inside the visible viewport. Positions are * normalized so a saved desktop location remains meaningful on smaller screens. */ export function clampHudPositionToViewport( position: NormalizedHudPosition, viewport: HudSize, panel: HudSize, padding = 6, ): NormalizedHudPosition { const viewportWidth = Math.max(1, finite(viewport.width, 1)); const viewportHeight = Math.max(1, finite(viewport.height, 1)); const horizontalInset = Math.min( 0.5, (Math.max(0, finite(panel.width, 0)) / 2 + Math.max(0, padding)) / viewportWidth, ); const verticalInset = Math.min( 0.5, (Math.max(0, finite(panel.height, 0)) / 2 + Math.max(0, padding)) / viewportHeight, ); const normalized = normalizeHudPosition(position); return { x: clamp(normalized.x, horizontalInset, 1 - horizontalInset), y: clamp(normalized.y, verticalInset, 1 - verticalInset), }; }