velxio/frontend/src/components/simulator/SensorControlPanel.tsx

180 lines
6.3 KiB
TypeScript
Raw Normal View History

/**
* 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<SensorControlPanelProps> = ({
componentId,
metadataId,
sensorName,
onClose,
}) => {
feat(editor): translate Oscilloscope + property dialog + selection bar + console + custom chips + sensor + board picker (Editor block 9) This commit closes the cluster of small editor surfaces that touch the active simulation experience. Every visible control on these panels now reads from t() keys. Translated: - Oscilloscope panel (title, Add Channel button + tooltip, Time/div label, Run / Pause toggle copy + tooltips, Clear, empty-state copy + hint, per-channel remove tooltip). - ComponentPropertyDialog (close, pin-roles header with two wire-mode variants, Arduino Pin label, rotate / delete buttons + the inline confirm-delete prompt with name interpolation). - SelectionActionBar (toolbar aria-label, Rotate / Delete / Deselect with kind-aware delete labels for wire / component / board). - CompilationConsole (Output title, error / warning badge counts with i18next pluralisation, filter dropdown, autoscroll label, Clear + Close icon tooltips, empty-state). - CustomChipDialog (header with chipName interpolation, Examples / Editor tabs, Attributes panel header, compile status messages including the "✓ Compiled — N KB" success line, footer Cancel / Save & Place / Compile first buttons). - SensorControlPanel (close button). - BoardPickerModal (Add Board heading). Translation pipeline - en.json gets the new keys hand-curated. - The 8 non-English locales were auto-translated via DeepSeek using the existing scripts/translate-i18n.mjs pipeline (one --force run, ~1 min total). Output validated with sameShape() before write so any LLM-introduced key drift would have failed loudly. Quality note - DeepSeek's translations now cover the entire bundle, including earlier hand-translated content. Tone may differ slightly from the prior hand passes but the meaning is consistent and brand / technical terms (Velxio, ngspice-WASM, ATmega328P, ESP32-C3, etc.) are preserved unchanged in every locale per the prompt invariants.
2026-05-09 22:25:29 +07:00
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<Record<string, number | boolean>>(() => {
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 (
<button
key={ctrl.key}
className="sensor-trigger-button"
onClick={() => handleButton(ctrl.key)}
>
{ctrl.label}
</button>
);
}
// 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 (
<div key={sc.key} className="sensor-control-row">
<span className={isAxisKey ? 'sensor-control-label' : 'sensor-control-label-wide'}>
{isAxisKey ? sc.label : sc.label}
</span>
<input
type="range"
className="sensor-slider"
min={sc.min}
max={sc.max}
step={sc.step}
value={val}
onChange={(e) => handleSlider(sc.key, e.target.value)}
/>
<span className="sensor-value-display">
{displayVal}
{sc.unit ? ` ${sc.unit}` : ''}
</span>
</div>
);
};
// 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 (
<React.Fragment key={section.label}>
<div className="sensor-section-label">
<span className="sensor-section-icon">{section.icon}</span>
{section.label}
</div>
{sectionControls.map(renderControl)}
</React.Fragment>
);
});
}
return def.controls.map(renderControl);
};
return (
<div
className="sensor-control-panel"
onClick={(e) => 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()}
>
<div className="sensor-panel-header">
<span className="sensor-panel-title">{sensorName || def.title}</span>
feat(editor): translate Oscilloscope + property dialog + selection bar + console + custom chips + sensor + board picker (Editor block 9) This commit closes the cluster of small editor surfaces that touch the active simulation experience. Every visible control on these panels now reads from t() keys. Translated: - Oscilloscope panel (title, Add Channel button + tooltip, Time/div label, Run / Pause toggle copy + tooltips, Clear, empty-state copy + hint, per-channel remove tooltip). - ComponentPropertyDialog (close, pin-roles header with two wire-mode variants, Arduino Pin label, rotate / delete buttons + the inline confirm-delete prompt with name interpolation). - SelectionActionBar (toolbar aria-label, Rotate / Delete / Deselect with kind-aware delete labels for wire / component / board). - CompilationConsole (Output title, error / warning badge counts with i18next pluralisation, filter dropdown, autoscroll label, Clear + Close icon tooltips, empty-state). - CustomChipDialog (header with chipName interpolation, Examples / Editor tabs, Attributes panel header, compile status messages including the "✓ Compiled — N KB" success line, footer Cancel / Save & Place / Compile first buttons). - SensorControlPanel (close button). - BoardPickerModal (Add Board heading). Translation pipeline - en.json gets the new keys hand-curated. - The 8 non-English locales were auto-translated via DeepSeek using the existing scripts/translate-i18n.mjs pipeline (one --force run, ~1 min total). Output validated with sameShape() before write so any LLM-introduced key drift would have failed loudly. Quality note - DeepSeek's translations now cover the entire bundle, including earlier hand-translated content. Tone may differ slightly from the prior hand passes but the meaning is consistent and brand / technical terms (Velxio, ngspice-WASM, ATmega328P, ESP32-C3, etc.) are preserved unchanged in every locale per the prompt invariants.
2026-05-09 22:25:29 +07:00
<button className="sensor-panel-close" onClick={onClose} title={t('editor.sensorPanel.close')}>
×
</button>
</div>
{renderControls()}
</div>
);
};