2026-03-19 11:52:46 +07:00
|
|
|
|
/**
|
|
|
|
|
|
* 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';
|
2026-05-10 05:16:03 +07:00
|
|
|
|
import { useTranslation } from 'react-i18next';
|
2026-03-19 11:52:46 +07:00
|
|
|
|
import {
|
2026-07-24 02:32:14 +07:00
|
|
|
|
getSensorControl,
|
2026-03-19 11:52:46 +07:00
|
|
|
|
type SensorControl,
|
|
|
|
|
|
type SliderControl,
|
|
|
|
|
|
} from '../../simulation/sensorControlConfig';
|
2026-05-17 08:28:52 +07:00
|
|
|
|
import { dispatchSensorUpdate, getLastSensorValues } from '../../simulation/SensorUpdateRegistry';
|
2026-03-19 11:52:46 +07:00
|
|
|
|
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'] },
|
2026-04-22 02:45:45 +07:00
|
|
|
|
{ label: 'Rotation', icon: '↻', keys: ['gyroX', 'gyroY', 'gyroZ'] },
|
|
|
|
|
|
{ label: 'Temperature', icon: '🌡', keys: ['temp'] },
|
2026-03-19 11:52:46 +07:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
// 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();
|
2026-07-24 02:32:14 +07:00
|
|
|
|
const def = getSensorControl(metadataId);
|
2026-03-19 11:52:46 +07:00
|
|
|
|
|
2026-05-17 08:28:52 +07:00
|
|
|
|
// 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 } : {};
|
|
|
|
|
|
});
|
2026-03-19 11:52:46 +07:00
|
|
|
|
|
2026-05-17 08:28:52 +07:00
|
|
|
|
// 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.
|
2026-03-19 11:52:46 +07:00
|
|
|
|
useEffect(() => {
|
2026-05-17 08:28:52 +07:00
|
|
|
|
if (def && Object.keys(def.defaultValues).length > 0 && !getLastSensorValues(componentId)) {
|
2026-03-19 11:52:46 +07:00
|
|
|
|
dispatchSensorUpdate(componentId, def.defaultValues);
|
|
|
|
|
|
}
|
2026-04-22 02:45:45 +07:00
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
2026-03-19 11:52:46 +07:00
|
|
|
|
}, [componentId]);
|
|
|
|
|
|
|
|
|
|
|
|
// Close on Escape
|
|
|
|
|
|
useEffect(() => {
|
2026-04-22 02:45:45 +07:00
|
|
|
|
const onKey = (e: KeyboardEvent) => {
|
|
|
|
|
|
if (e.key === 'Escape') onClose();
|
|
|
|
|
|
};
|
2026-03-19 11:52:46 +07:00
|
|
|
|
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);
|
2026-04-22 02:45:45 +07:00
|
|
|
|
setValues((prev) => ({ ...prev, [key]: v }));
|
2026-03-19 11:52:46 +07:00
|
|
|
|
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;
|
2026-04-22 02:45:45 +07:00
|
|
|
|
const val = (values[sc.key] as number) ?? sc.defaultValue;
|
2026-03-19 11:52:46 +07:00
|
|
|
|
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}
|
2026-04-22 02:45:45 +07:00
|
|
|
|
onChange={(e) => handleSlider(sc.key, e.target.value)}
|
2026-03-19 11:52:46 +07:00
|
|
|
|
/>
|
|
|
|
|
|
<span className="sensor-value-display">
|
2026-04-22 02:45:45 +07:00
|
|
|
|
{displayVal}
|
|
|
|
|
|
{sc.unit ? ` ${sc.unit}` : ''}
|
2026-03-19 11:52:46 +07:00
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// For MPU6050 render sections; for everything else render controls flat
|
|
|
|
|
|
const renderControls = () => {
|
|
|
|
|
|
if (metadataId === 'mpu6050') {
|
2026-04-22 02:45:45 +07:00
|
|
|
|
return MPU6050_SECTIONS.map((section) => {
|
|
|
|
|
|
const sectionControls = def.controls.filter((c) => section.keys.includes(c.key));
|
2026-03-19 11:52:46 +07:00
|
|
|
|
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 (
|
2026-05-17 06:56:24 +07:00
|
|
|
|
<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()}
|
|
|
|
|
|
>
|
2026-03-19 11:52:46 +07:00
|
|
|
|
<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')}>
|
2026-04-22 02:45:45 +07:00
|
|
|
|
×
|
|
|
|
|
|
</button>
|
2026-03-19 11:52:46 +07:00
|
|
|
|
</div>
|
|
|
|
|
|
{renderControls()}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|