/** * SensorControlPanel — wokwi-style interactive sensor controls. * * Appears at the top-left of the simulation canvas when a sensor component is * clicked during simulation. Provides sliders and buttons that feed values * directly into the running simulation via SensorUpdateRegistry. */ import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { getSensorControl, type SensorControl, type SliderControl, } from '../../simulation/sensorControlConfig'; import { dispatchSensorUpdate, getLastSensorValues } from '../../simulation/SensorUpdateRegistry'; import './SensorControlPanel.css'; interface SensorControlPanelProps { componentId: string; metadataId: string; sensorName: string; onClose: () => void; } // ── Section grouping for MPU6050 ──────────────────────────────────────────── interface SensorSection { label: string; icon: string; keys: string[]; } const MPU6050_SECTIONS: SensorSection[] = [ { label: 'Acceleration', icon: '↗', keys: ['accelX', 'accelY', 'accelZ'] }, { label: 'Rotation', icon: '↻', keys: ['gyroX', 'gyroY', 'gyroZ'] }, { label: 'Temperature', icon: '🌡', keys: ['temp'] }, ]; // Keys that use single-char axis labels (X / Y / Z) rather than the full key name const AXIS_KEYS = new Set(['accelX', 'accelY', 'accelZ', 'gyroX', 'gyroY', 'gyroZ']); // ── Component ─────────────────────────────────────────────────────────────── export const SensorControlPanel: React.FC = ({ componentId, metadataId, sensorName, onClose, }) => { const { t } = useTranslation(); const def = getSensorControl(metadataId); // Local slider/button state — hydrated from the registry's last-known // values for this componentId (so reopening a sensor or switching between // two sensors of the same type shows each one's current state, not the // previous panel's). Falls back to config defaults the first time a // sensor is opened. const [values, setValues] = useState>(() => { const cached = getLastSensorValues(componentId); if (cached) return { ...(def?.defaultValues ?? {}), ...cached }; return def ? { ...def.defaultValues } : {}; }); // Push defaults into simulation on first open for this sensor. Skipped // when the sensor already has cached values — the simulation still holds // them, no need to clobber. useEffect(() => { if (def && Object.keys(def.defaultValues).length > 0 && !getLastSensorValues(componentId)) { dispatchSensorUpdate(componentId, def.defaultValues); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [componentId]); // Close on Escape useEffect(() => { const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [onClose]); if (!def) return null; // ── Handlers ────────────────────────────────────────────────────────────── const handleSlider = (key: string, raw: string) => { const v = parseFloat(raw); setValues((prev) => ({ ...prev, [key]: v })); dispatchSensorUpdate(componentId, { [key]: v }); }; const handleButton = (key: string) => { dispatchSensorUpdate(componentId, { [key]: true }); }; // ── Render helpers ──────────────────────────────────────────────────────── const renderControl = (ctrl: SensorControl) => { if (ctrl.type === 'button') { return ( ); } // Slider const sc = ctrl as SliderControl; const val = (values[sc.key] as number) ?? sc.defaultValue; const displayVal = sc.formatValue ? sc.formatValue(val) : String(val); const isAxisKey = AXIS_KEYS.has(sc.key); return (
{isAxisKey ? sc.label : sc.label} handleSlider(sc.key, e.target.value)} /> {displayVal} {sc.unit ? ` ${sc.unit}` : ''}
); }; // For MPU6050 render sections; for everything else render controls flat const renderControls = () => { if (metadataId === 'mpu6050') { return MPU6050_SECTIONS.map((section) => { const sectionControls = def.controls.filter((c) => section.keys.includes(c.key)); return (
{section.icon} {section.label}
{sectionControls.map(renderControl)}
); }); } return def.controls.map(renderControl); }; return (
e.stopPropagation()} // The canvas treats left mousedown on empty space as a pan gesture. // Without stopping mousedown here, dragging the lux/temp/etc. slider // thumb pans the canvas instead of moving the slider. onMouseDown={(e) => e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()} >
{sensorName || def.title}
{renderControls()}
); };