reachy-blocks / src /app /boot.ts
Claude
feat: emotions-library loader, sound effects, WAV pipeline
bac98f7 unverified
Raw
History Blame Contribute Delete
2.68 kB
// Shared composition root for practice and embed modes: builds the UI
// shell, injects the Blockly workspace, constructs the robot adapter,
// and wires the interpreter Runner to the Run/Stop button.
import * as Blockly from "blockly";
import type { RobotAdapter } from "../robot/adapter";
import { CommandLog } from "../util/log";
import { createWorkspace, loadProgram, setWorkspaceTheme } from "../blocks/workspace";
import type { EmotionSource, SoundSource } from "../interpreter/commands";
import { Runner } from "../interpreter/runner";
import { STARTER_PROGRAM } from "./starter";
import { buildAppShell, type AppShell } from "./ui";
export interface BootOptions {
/** Create the robot once the puppet's host element exists. */
attachRobot(puppetHost: HTMLElement, log: CommandLog): RobotAdapter;
theme: "light" | "dark";
emotions: EmotionSource;
sounds: SoundSource;
/** Skip localStorage restore (e2e hermetic runs). */
fresh?: boolean;
}
export interface App {
shell: AppShell;
workspace: Blockly.WorkspaceSvg;
robot: RobotAdapter;
runner: Runner;
log: CommandLog;
setTheme(theme: "light" | "dark"): void;
loadProgram(state: object): void;
/** Override the coin-flip RNG (deterministic tests). */
setRandom(fn: () => number): void;
}
export function bootApp(root: HTMLElement, opts: BootOptions): App {
const shell = buildAppShell(root);
const log = new CommandLog();
const robot = opts.attachRobot(shell.puppetHost, log);
const workspace = createWorkspace(shell.workspaceHost, {
theme: opts.theme,
fresh: opts.fresh,
starter: STARTER_PROGRAM,
});
window.addEventListener("resize", () => Blockly.svgResize(workspace));
let random: () => number = Math.random;
const runner = new Runner(workspace, robot, {
emotions: opts.emotions,
sounds: opts.sounds,
random: () => random(),
hooks: {
onHighlight: (id) => workspace.highlightBlock(id),
onRunStateChange: (running) => shell.setRunning(running),
},
});
shell.onRunClick(() => {
if (runner.running) void runner.stop();
else void runner.run();
});
// Warm the emotion cache in the background so the first "act happy"
// doesn't stall on the network.
(opts.emotions as { prefetchAll?: () => void }).prefetchAll?.();
return {
shell,
workspace,
robot,
runner,
log,
setTheme(theme: "light" | "dark"): void {
document.documentElement.setAttribute("data-theme", theme);
setWorkspaceTheme(workspace, theme);
},
loadProgram(state: object): void {
loadProgram(workspace, state);
},
setRandom(fn: () => number): void {
random = fn;
},
};
}