microduck-reachy-simulator / src /ui /ReachyControls.jsx
laureen
Withdraw Code Training from published main pending fixes
99e34b4
Raw
History Blame Contribute Delete
4.09 kB
import { useState } from "react";
import { gameApi, useGame } from "../store.js";
import { HOME } from "../game/reachy/reachyKinematics.js";
import { POSES, SEQUENCES } from "../game/reachy/reachyMoves.js";
import SimulatorPanel from "./SimulatorPanel.jsx";
const FIELDS = [
["x", "x (mm)", -20, 20], ["y", "y (mm)", -20, 20], ["z", "z (mm)", -30, 20],
["roll", "Roll (°)", -25, 25], ["pitch", "Pitch (°)", -25, 25], ["yaw", "Yaw (°)", -45, 45],
["leftAntenna", "Left antenna (°)", -90, 90], ["rightAntenna", "Right antenna (°)", -90, 90],
];
export default function ReachyControls() {
const [pose, setPose] = useState({ ...HOME });
const [duration, setDuration] = useState(1);
const [error, setError] = useState(null);
const reachy = useGame((s) => s.reachy);
const locked = useGame((s) => s.emotionBusy || s.scriptPlaying);
const recording = useGame((s) => s.scriptRecording);
const [speech, setSpeech] = useState("");
const act = async (fn) => {
setError(null);
try { await fn(gameApi.reachy); }
catch (e) { if (e.name !== "AbortError") setError(e.message); }
};
const action = (label, fn) => recording ? gameApi.script?.add(label, () => act(fn)) : act(fn);
return <SimulatorPanel side="left" title="Reachy Mini">
<fieldset disabled={locked || (reachy.playing && !recording)}>
<legend>Poses</legend>
<div className="sim-buttons">{Object.entries(POSES).map(([name, p]) => <button key={name} onClick={() => action(name, (r) => r.playSequence([{ pose: { ...HOME, ...p }, duration }]))}>{name}</button>)}</div>
<legend>Sequences</legend>
<div className="sim-buttons">{Object.entries(SEQUENCES).map(([name, sequence]) => <button key={name} onClick={() => action(name, (r) => r.playSequence(sequence))}>{name}</button>)}</div>
<legend>Emotions</legend>
<div className="sim-buttons">{[["joy", "Happy"], ["sadness", "Sad"], ["anger", "Angry"]].map(([id, label]) => <button key={id} onClick={() => action(`Reachy ${label}`, () => gameApi.playReachyEmotion?.(id))}>{label}</button>)}</div>
</fieldset>
<fieldset disabled={locked}>
<legend>Custom pose</legend>
{FIELDS.map(([key, label, min, max]) => <label className="sim-field" key={key}><span>{label}</span><input type="number" min={min} max={max} step="1" value={pose[key]} onChange={(e) => setPose({ ...pose, [key]: Number(e.target.value) })} /></label>)}
<label className="sim-field"><span>Duration (s)</span><input type="number" min="0.1" max="30" step="0.1" value={duration} onChange={(e) => setDuration(Number(e.target.value))} /></label>
<label className="sim-field"><span>Playback speed</span><select value={reachy.speed} onChange={(e) => act((r) => r.setSpeed(Number(e.target.value)))}>{[0.25, 0.5, 1, 1.5, 2].map((s) => <option key={s} value={s}>{s}×</option>)}</select></label>
<div className="sim-buttons">
<button onClick={() => action("Reachy custom pose", (r) => r.enqueue(pose, duration))}>Queue</button>
<button disabled={reachy.playing && !recording} onClick={() => action("Play Reachy queue", (r) => reachy.queue.length ? r.playQueue() : r.playSequence([{ pose, duration }]))}>Play</button>
<button onClick={() => gameApi.reachy?.stop()}>Stop</button>
<button onClick={() => gameApi.reachy?.clear()}>Clear</button>
</div>
</fieldset>
<fieldset disabled={locked}>
<legend>Text to speech</legend>
<input className="sim-speech" value={speech} placeholder="What should Reachy say?" onChange={(e) => setSpeech(e.target.value)} />
<button disabled={!speech.trim()} onClick={() => action(`Reachy says: ${speech.trim()}`, () => gameApi.reachySay?.(speech.trim()))}>Say / add to script</button>
</fieldset>
<p role="status">{reachy.playing ? "Playing · " : ""}{reachy.queue.length} queued</p>
{reachy.queue.length > 0 && <ol className="sim-queue">{reachy.queue.map((label, i) => <li key={i}>{label}</li>)}</ol>}
{(error || reachy.error) && <p className="sim-error" role="alert">{error || reachy.error}</p>}
</SimulatorPanel>;
}