| 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>; |
| } |
|
|