diff --git a/frontend/src/components/simulator/SensorControlPanel.tsx b/frontend/src/components/simulator/SensorControlPanel.tsx index f7e97e6f..7824545d 100644 --- a/frontend/src/components/simulator/SensorControlPanel.tsx +++ b/frontend/src/components/simulator/SensorControlPanel.tsx @@ -13,7 +13,7 @@ import { type SensorControl, type SliderControl, } from '../../simulation/sensorControlConfig'; -import { dispatchSensorUpdate } from '../../simulation/SensorUpdateRegistry'; +import { dispatchSensorUpdate, getLastSensorValues } from '../../simulation/SensorUpdateRegistry'; import './SensorControlPanel.css'; interface SensorControlPanelProps { @@ -51,14 +51,22 @@ export const SensorControlPanel: React.FC = ({ const { t } = useTranslation(); const def = SENSOR_CONTROLS[metadataId]; - // Local slider/button state — initialised from config defaults - const [values, setValues] = useState>( - def ? { ...def.defaultValues } : {}, - ); + // 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 initial defaults into simulation on mount + // 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) { + if (def && Object.keys(def.defaultValues).length > 0 && !getLastSensorValues(componentId)) { dispatchSensorUpdate(componentId, def.defaultValues); } // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/frontend/src/components/simulator/SimulatorCanvas.tsx b/frontend/src/components/simulator/SimulatorCanvas.tsx index 462b9524..16551091 100644 --- a/frontend/src/components/simulator/SimulatorCanvas.tsx +++ b/frontend/src/components/simulator/SimulatorCanvas.tsx @@ -2382,13 +2382,18 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => { : 'default', }} > - {/* Sensor Control Panel — shown when a sensor component is clicked during simulation */} + {/* Sensor Control Panel — shown when a sensor component is clicked during simulation. + key={sensorControlComponentId} forces a fresh mount when the user clicks a + different instance of the same sensor type (e.g. a second photoresistor); the + slider state is local and would otherwise show the previously-clicked sensor's + value until the user manually moved it. */} {sensorControlComponentId && sensorControlMetadataId && (() => { const meta = registry.getById(sensorControlMetadataId); return ( ) => void; +type SensorValues = Record; +type SensorUpdateCallback = (values: SensorValues) => void; const registry = new Map(); +const lastValues = new Map(); /** * Register a callback for a component. Called from inside attachEvents(). @@ -20,19 +22,31 @@ export function registerSensorUpdate(componentId: string, cb: SensorUpdateCallba /** * Dispatch new sensor values for a component. Called from SensorControlPanel. - * No-ops silently if the component has no registered callback. + * No-ops silently if the component has no registered callback. Values are + * also cached so the panel can rehydrate the slider when reopened on the + * same sensor (or when switching between sensors of the same type). */ -export function dispatchSensorUpdate( - componentId: string, - values: Record, -): void { +export function dispatchSensorUpdate(componentId: string, values: SensorValues): void { registry.get(componentId)?.(values); + const prev = lastValues.get(componentId); + lastValues.set(componentId, prev ? { ...prev, ...values } : { ...values }); +} + +/** + * Read the last values dispatched for a component. Returns undefined if the + * component has never received a dispatch. Used by SensorControlPanel to + * restore slider state when reopened. + */ +export function getLastSensorValues(componentId: string): SensorValues | undefined { + return lastValues.get(componentId); } /** * Unregister a component's callback. Called in the cleanup function returned * by attachEvents() so stale callbacks don't persist after simulation stops. + * Values are also cleared so a deleted/recreated component starts fresh. */ export function unregisterSensorUpdate(componentId: string): void { registry.delete(componentId); + lastValues.delete(componentId); }