import React from 'react';
function TopDownMap({ sensors = {}, variant = 'full', size: overrideSize, overlay = false }) {
const size = overrideSize || (variant === 'mini' ? 190 : 260);
const center = size / 2;
const offsetY = size * 0.07; // nudge everything downward within the viewBox
const centerX = center;
const centerY = center + offsetY;
const innerCircle = center * 0.8; // keep proportions consistent as size changes
const lightRingInner = innerCircle - 4;
const lightRingOuter = innerCircle + 4;
const cliffRingInner = innerCircle - 14;
const cliffRingOuter = innerCircle - 6;
const wheelLineOffset = innerCircle * 0.65;
const bumps = sensors?.bumpsAndWheelDrops || {};
const wheelOver = sensors?.wheelOvercurrents || {};
const wheelCurrentLeft = sensors?.wheelLeftCurrentMa ?? 0;
const wheelCurrentRight = sensors?.wheelRightCurrentMa ?? 0;
const sideBrushCurrent = sensors?.sideBrushCurrentMa ?? 0;
const mainBrushCurrent = sensors?.mainBrushCurrentMa ?? 0;
const bumpDepress = 6;
const bumpLeftOffset = bumps.bumpLeft ? bumpDepress : 0;
const bumpRightOffset = bumps.bumpRight ? bumpDepress : 0;
const lightAngles = buildSegments({
count: 6,
totalSpan: 140,
gap: 6,
startAngle: -70,
});
const lightLabels = ['L', 'FL', 'CL', 'CR', 'FR', 'R'];
const lightValues = [
sensors?.lightBumpLeftSignal,
sensors?.lightBumpFrontLeftSignal,
sensors?.lightBumpCenterLeftSignal,
sensors?.lightBumpCenterRightSignal,
sensors?.lightBumpFrontRightSignal,
sensors?.lightBumpRightSignal,
];
const lightSegments = lightAngles.map((ang, idx) => ({
label: lightLabels[idx],
start: ang.start,
end: ang.end,
value: lightValues[idx],
}));
const cliffSegments = [
{ label: 'Cliff L', start: -60, end: -46, value: sensors?.cliffLeftSignal, active: sensors?.cliffLeft },
{ label: 'Cliff FL', start: -32, end: -16, value: sensors?.cliffFrontLeftSignal, active: sensors?.cliffFrontLeft },
{ label: 'Cliff FR', start: 16, end: 32, value: sensors?.cliffFrontRightSignal, active: sensors?.cliffFrontRight },
{ label: 'Cliff R', start: 46, end: 60, value: sensors?.cliffRightSignal, active: sensors?.cliffRight },
];
const lightMaxSamples = lightValues.filter((v) => v != null);
const maxLight = lightMaxSamples.length ? Math.max(...lightMaxSamples, 1200) : 1200;
return (
{!overlay ? (
<>
Top-down
0–{maxLight}
>
) : null}
);
}
function ArcSegment({ cx, cy, rInner, rOuter, startDeg, endDeg, color, pulse = false, opacity = 1 }) {
const rMid = (rInner + rOuter) / 2;
const strokeWidth = rOuter - rInner;
const path = describeArc(cx, cy, rMid, startDeg, endDeg);
return (
<>
{pulse ? (
) : null}
>
);
}
function ConeSegment({ cx, cy, rBase, rTip, startDeg, endDeg, color, value, max }) {
const mid = (startDeg + endDeg) / 2;
const baseA = polarToCartesian(cx, cy, rBase, startDeg);
const baseB = polarToCartesian(cx, cy, rBase, endDeg);
const tip = polarToCartesian(cx, cy, rTip, mid);
const norm = clamp01(value != null ? value / (max || 1) : 0);
const eased = Math.pow(norm, 0.35); // even more sensitive near the start, softer near the end
// higher value = closer obstacle → bar shrinks toward the tip (reverse fill)
const filledR = rBase - (rBase - rTip) * eased;
const barR = Math.max(rTip, Math.min(filledR, rBase));
const filledA = polarToCartesian(cx, cy, filledR, startDeg);
const filledB = polarToCartesian(cx, cy, filledR, endDeg);
const fg = `M ${tip.x} ${tip.y} L ${filledA.x} ${filledA.y} L ${filledB.x} ${filledB.y} Z`;
return ;
}
function describeArc(cx, cy, r, startDeg, endDeg) {
let start = startDeg;
let end = endDeg;
if (end <= start) end += 360;
const startPt = polarToCartesian(cx, cy, r, start);
const endPt = polarToCartesian(cx, cy, r, end);
return ['M', startPt.x, startPt.y, 'A', r, r, 0, 0, 1, endPt.x, endPt.y].join(' ');
}
function polarToCartesian(cx, cy, r, angleDeg) {
const rad = toRad(angleDeg);
return {
x: cx + r * Math.cos(rad),
y: cy + r * Math.sin(rad),
};
}
function toRad(angleDeg) {
return ((angleDeg - 90) * Math.PI) / 180;
}
function currentColor(value, overcurrent) {
if (overcurrent) return '#ef4444';
const mag = Math.abs(value);
if (mag > 900) return '#f59e0b';
if (mag > 300) return '#22c55e';
return '#64748b';
}
function clamp01(value) {
return Math.max(0, Math.min(1, value));
}
function WheelVisual({ cx, cy, current, drop, overcurrent, label }) {
const mag = Math.abs(current);
const pct = clamp01(mag / 1200);
const color = currentColor(current, overcurrent);
const barH = 56;
const currentW = 14;
const dropW = 9;
const gap = 3;
const currentFill = barH * pct;
const sign = label === 'L' ? -1 : 1;
const currentCenterX = sign * (-dropW / 2 - gap / 2);
const dropCenterX = sign * (currentW / 2 + gap / 2);
const groupWidth = currentW + dropW + gap + 2;
const groupHeight = barH + 4;
return (
{label}
);
}
function SideBrushVisual({ cx, cy, current, overcurrent }) {
var mag = Math.abs(current);
if (mag < 10) mag = 0;
const color = currentColor(current * 3, overcurrent);
const armLength = 43;
const spinDuration = mag > 0 ? 0.65 : null;
const spinDirection = 'reverse';
return (
{[0, 120, 240].map((deg) => {
const rad = toRad(deg);
const x2 = cx + armLength * Math.cos(rad);
const y2 = cy + armLength * Math.sin(rad);
return ;
})}
{overcurrent ? : null}
);
}
function MainBrushVisual({ cx, cy, current, overcurrent, variant, dirtLeft, dirtRight }) {
const mag = Math.abs(current);
const color = currentColor(current, overcurrent);
const opacity = 1;
const rollerWidth = 96;
const rollerHeight = 12;
const patternA = `main-brush-pattern-a-${variant}`;
const patternB = `main-brush-pattern-b-${variant}`;
const dur = mag > 0 ? 0.6 : null;
const dir = current >= 0 ? 1 : -1;
return (
{[patternA, patternB].map((id, idx) => (
{dur ? (
) : null}
))}
{overcurrent ? (
) : null}
);
}
function lightBumpColor(value, max) {
if (value == null || value <= 0) return 'hsl(200 60% 18%)'; // faint blue instead of black
const maxVal = max || 1;
const norm = clamp01(value / maxVal);
const eased = Math.pow(norm, 0.45); // match cone fill sensitivity
// offset the start hue so we don't begin at red, still sweep the full rainbow
const startHue = 200; // deep cyan/blue start
const hue = (startHue + eased * 360) % 360;
return `hsl(${hue} 100% 60%)`;
}
function cliffColor(value, active) {
if (active) return '#ef4444';
const t = clamp01(value == null ? 0 : value / 4095);
const start = [47, 55, 69];
const end = [245, 158, 11];
const [r, g, b] = start.map((s, i) => Math.round(s + (end[i] - s) * t));
return `rgb(${r}, ${g}, ${b})`;
}
function buildSegments({ count, totalSpan, gap, startAngle }) {
const usable = totalSpan - gap * (count - 1);
const width = usable / count;
const segments = [];
let cursor = startAngle;
for (let i = 0; i < count; i += 1) {
const end = cursor + width;
segments.push({ start: cursor, end });
cursor = end + gap;
}
return segments;
}
export default TopDownMap;