From 433a4989e1670f11f9df31315e9fcc65c4384337 Mon Sep 17 00:00:00 2001 From: monkey-w1n5t0n Date: Sun, 28 Jun 2026 04:24:48 +0200 Subject: [PATCH] feat(inputs): wire input layer into console + flesh out INPUTS drawer Route onMove through the XY-pad source's pushPad; instantiate useInputLayer per engine and expose it on ConsoleCtx. The Inputs drawer now picks the source(s) (XY pad / MIDI / gamepad toggles), configures each (gamepad single/double stick; MIDI learn-map with per-binding clear), and shows the composed channel layout + the blend-to-2 reshape notice referencing the multi-WASM TODO (inputs-spec). --- manifold/src/console/ConsoleApp.tsx | 14 ++- manifold/src/console/Drawers.tsx | 186 ++++++++++++++++++++++++---- manifold/src/console/types.ts | 5 + 3 files changed, 179 insertions(+), 26 deletions(-) diff --git a/manifold/src/console/ConsoleApp.tsx b/manifold/src/console/ConsoleApp.tsx index ef3b4cb..b9bd39c 100644 --- a/manifold/src/console/ConsoleApp.tsx +++ b/manifold/src/console/ConsoleApp.tsx @@ -51,6 +51,7 @@ import { FeedbackController, type ProtoFeedbackMode } from '../feedback'; import { DEFAULT_OUTPUT_MODE, OUTPUT_MODES, outputModeDescriptor } from './output-mode'; import { useSettings, resolveInputMap } from '../settings/settings-store'; import { useBackendManager } from '../backends'; +import { useInputLayer } from '../inputs'; let SNAP_ID = 0; @@ -226,9 +227,17 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp // rAF loop; the Editor serial protocol remains its own panel. const setOutputMode = (m: OutputMode) => setOutputModeState(m); - // Drive a pad/joystick/manifold move through the real engine, then mirror the - // raw position into React state for readouts. + // Modular input layer (workstream F): composes the active input SOURCE(s) + // (XY pad / MIDI / gamepad) into the engine. The XY pad is push-driven via + // `pushPad`; MIDI + gamepad are pulled by the layer's own rAF loop. + const inputs = useInputLayer(engine); + + // Drive a pad/joystick/manifold move through the input layer's XY-pad source, + // then mirror the raw position into React state for readouts. The layer's loop + // composes it with any other active sources and writes to the engine; we still + // do a direct setInput so the pad feels instant even when it's the sole source. const onMove = (x: number, y: number) => { + inputs.pushPad(x, y); engine?.setInput(x, y); setPos([x, y]); }; @@ -576,6 +585,7 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp setOscSendRaw, setParams: (next: MFParam[]) => setParams(next), markers, + inputs, health, gradient: gradient.norms, gradientStatus: gradient.status, diff --git a/manifold/src/console/Drawers.tsx b/manifold/src/console/Drawers.tsx index 9c583fc..1325a15 100644 --- a/manifold/src/console/Drawers.tsx +++ b/manifold/src/console/Drawers.tsx @@ -225,37 +225,175 @@ function LearningDrawer(ctx: ConsoleCtx, depth: DrawerDepth) { } // =========================================================================== -// 2. INPUTS (dock-spec §2 — workstream F territory, referenced) +// 2. INPUTS (workstream F; inputs-spec — modular input layer) // =========================================================================== +const STATUS_TONE: Record = { + ready: 'var(--accent)', + connecting: 'var(--warn)', + unavailable: 'var(--fg-dim)', + error: 'var(--danger)', + idle: 'var(--fg-dim)', +}; + function InputsDrawer(ctx: ConsoleCtx, depth: DrawerDepth) { - const src = - ctx.mode.input === 'joystick' ? 'Joystick' : ctx.mode.input === 'audio_in' ? 'Mic (1-input)' : 'XY pad'; + const inp = ctx.inputs; + const enabledCount = inp.sources.filter((s) => s.enabled).length; + const reshaping = inp.axisCount > inp.engineInputSize; + return ( <> -
- {src} - 2 inputs +
+ + {enabledCount === 0 ? 'no source' : `${enabledCount} source${enabledCount > 1 ? 's' : ''}`} + + {inp.axisCount} axes + engine: {inp.engineInputSize}-in + {reshaping && blended → {inp.engineInputSize}}
- Source - { - /* TODO(workstream F, inputs-spec): MIDI / gamepad / hands sources land here; the - input source is currently fixed by the mode. engine.setInput(x,y) is the only door. */ - }} - options={[ - { value: 'xy', label: 'XY pad' }, - { value: 'joystick', label: 'Joystick' }, - { value: 'audio_in', label: 'Mic' }, - ]} - /> + + Sources +
+ {inp.sources.map((s) => ( +
+
+ + {s.label} + {s.enabled ? ` · ${s.axisCount} ax` : ''} + + {depth !== 'peek' && ( + + {s.status.message} + + )} +
+ inp.setEnabled(s.kind, v)} label="" /> +
+ ))} +
+ {depth !== 'peek' && ( -

- v1 browser is fixed-2-input ({`MLP<2,…>`}). The modular N×M input matrix, per-axis pipeline - (deadzone → zoom → curve → smoothing → momentum) and MIDI / gamepad sources are owned by - workstream F (inputs-spec); this drawer fixes the dock shape only. -

+ <> + {/* ---- Gamepad config ---- */} + {inp.sources.find((s) => s.kind === 'gamepad')?.enabled && ( + <> + Gamepad · sticks + + + )} + + {/* ---- MIDI learn-map ---- */} + {inp.sources.find((s) => s.kind === 'midi')?.enabled && ( + <> + MIDI · learn-map +
+ + {inp.midiBindings.length > 0 && ( + + )} +
+ {inp.midiBindings.length === 0 ? ( +

+ No axes learned yet — arm Learn, then wiggle a knob or hit a pad. CCs map to a + continuous axis; notes map to a gate (1 while held). +

+ ) : ( +
+ {inp.midiBindings.map((b, i) => ( +
+ + {b.label} · {b.value.toFixed(2)} + + +
+ ))} +
+ )} + {inp.midiInputs.length > 0 && ( +

+ Listening on: {inp.midiInputs.map((p) => p.name).join(', ')} +

+ )} + + )} + + {/* ---- Channel layout ---- */} + Channel layout + {inp.channelLayout.length === 0 ? ( +

+ No active axes. Enable a source above. +

+ ) : ( +
+ {inp.channelLayout.map((c, i) => ( + + {i}: {c.source}·{c.label} + + ))} +
+ )} + +

+ The active sources concatenate into one input vector at the head of the spine. + {reshaping + ? ` The browser WASM is a fixed ${inp.engineInputSize}-input head (MLP<2,…>), so the + ${inp.axisCount} axes are blended down to ${inp.engineInputSize} (even→X / odd→Y mean).` + : ''}{' '} + {/* TODO(workstream F, docs/redesign/inputs-spec.md — "multiple WASM modules + + warm-start"): give every axis its own genuine input dimension by (re)loading a + WASM module whose MLP arity matches axisCount and warm-starting from the prior + net. Deferred — the reduction lives in InputLayer.compose(). */} + True per-axis dimensions land with the multi-WASM reshape (inputs-spec). +

+ )} ); diff --git a/manifold/src/console/types.ts b/manifold/src/console/types.ts index 4d45dbd..4a914c5 100644 --- a/manifold/src/console/types.ts +++ b/manifold/src/console/types.ts @@ -5,6 +5,7 @@ import type { MFMode, MFParam } from './model'; import type { BackendId } from '../dock/output-state'; import type { FeedbackMode } from '../engine/types'; import type { BackendStatus } from '../backends/backend'; +import type { UseInputLayer } from '../inputs'; /** The two product feedback modes (dock-spec §1.1; rl-feedback-design §0). */ export type FeedbackModeUI = 'explore-and-place' | 'geometric-dislike'; @@ -110,6 +111,10 @@ export interface ConsoleCtx { /** Markers plotted at the input location where each feedback was given. */ markers: FeedbackMarker[]; + // ---- Modular input layer (workstream F; inputs-spec) ---- + /** The composed input layer: source enable/config/status + channel layout. */ + inputs: UseInputLayer; + health: number; gradient: number[]; gradientStatus: string[];