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).
This commit is contained in:
parent
091cfe2716
commit
433a4989e1
3 changed files with 179 additions and 26 deletions
|
|
@ -51,6 +51,7 @@ import { FeedbackController, type ProtoFeedbackMode } from '../feedback';
|
||||||
import { DEFAULT_OUTPUT_MODE, OUTPUT_MODES, outputModeDescriptor } from './output-mode';
|
import { DEFAULT_OUTPUT_MODE, OUTPUT_MODES, outputModeDescriptor } from './output-mode';
|
||||||
import { useSettings, resolveInputMap } from '../settings/settings-store';
|
import { useSettings, resolveInputMap } from '../settings/settings-store';
|
||||||
import { useBackendManager } from '../backends';
|
import { useBackendManager } from '../backends';
|
||||||
|
import { useInputLayer } from '../inputs';
|
||||||
|
|
||||||
let SNAP_ID = 0;
|
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.
|
// rAF loop; the Editor serial protocol remains its own panel.
|
||||||
const setOutputMode = (m: OutputMode) => setOutputModeState(m);
|
const setOutputMode = (m: OutputMode) => setOutputModeState(m);
|
||||||
|
|
||||||
// Drive a pad/joystick/manifold move through the real engine, then mirror the
|
// Modular input layer (workstream F): composes the active input SOURCE(s)
|
||||||
// raw position into React state for readouts.
|
// (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) => {
|
const onMove = (x: number, y: number) => {
|
||||||
|
inputs.pushPad(x, y);
|
||||||
engine?.setInput(x, y);
|
engine?.setInput(x, y);
|
||||||
setPos([x, y]);
|
setPos([x, y]);
|
||||||
};
|
};
|
||||||
|
|
@ -576,6 +585,7 @@ export function ConsoleApp({ focus: initialFocus = 'composite' }: ConsoleAppProp
|
||||||
setOscSendRaw,
|
setOscSendRaw,
|
||||||
setParams: (next: MFParam[]) => setParams(next),
|
setParams: (next: MFParam[]) => setParams(next),
|
||||||
markers,
|
markers,
|
||||||
|
inputs,
|
||||||
health,
|
health,
|
||||||
gradient: gradient.norms,
|
gradient: gradient.norms,
|
||||||
gradientStatus: gradient.status,
|
gradientStatus: gradient.status,
|
||||||
|
|
|
||||||
|
|
@ -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<string, string> = {
|
||||||
|
ready: 'var(--accent)',
|
||||||
|
connecting: 'var(--warn)',
|
||||||
|
unavailable: 'var(--fg-dim)',
|
||||||
|
error: 'var(--danger)',
|
||||||
|
idle: 'var(--fg-dim)',
|
||||||
|
};
|
||||||
|
|
||||||
function InputsDrawer(ctx: ConsoleCtx, depth: DrawerDepth) {
|
function InputsDrawer(ctx: ConsoleCtx, depth: DrawerDepth) {
|
||||||
const src =
|
const inp = ctx.inputs;
|
||||||
ctx.mode.input === 'joystick' ? 'Joystick' : ctx.mode.input === 'audio_in' ? 'Mic (1-input)' : 'XY pad';
|
const enabledCount = inp.sources.filter((s) => s.enabled).length;
|
||||||
|
const reshaping = inp.axisCount > inp.engineInputSize;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||||
<Badge tone="info">{src}</Badge>
|
<Badge tone="info">
|
||||||
<Chip>2 inputs</Chip>
|
{enabledCount === 0 ? 'no source' : `${enabledCount} source${enabledCount > 1 ? 's' : ''}`}
|
||||||
|
</Badge>
|
||||||
|
<Chip tone="var(--accent)">{inp.axisCount} axes</Chip>
|
||||||
|
<Chip>engine: {inp.engineInputSize}-in</Chip>
|
||||||
|
{reshaping && <Chip tone="var(--warn)">blended → {inp.engineInputSize}</Chip>}
|
||||||
</div>
|
</div>
|
||||||
<SectionLabel>Source</SectionLabel>
|
|
||||||
<Segmented
|
<SectionLabel>Sources</SectionLabel>
|
||||||
value={ctx.mode.input}
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||||
onChange={() => {
|
{inp.sources.map((s) => (
|
||||||
/* TODO(workstream F, inputs-spec): MIDI / gamepad / hands sources land here; the
|
<div
|
||||||
input source is currently fixed by the mode. engine.setInput(x,y) is the only door. */
|
key={s.kind}
|
||||||
}}
|
style={{
|
||||||
options={[
|
display: 'flex',
|
||||||
{ value: 'xy', label: 'XY pad' },
|
alignItems: 'center',
|
||||||
{ value: 'joystick', label: 'Joystick' },
|
justifyContent: 'space-between',
|
||||||
{ value: 'audio_in', label: 'Mic' },
|
gap: 8,
|
||||||
]}
|
padding: '4px 8px',
|
||||||
/>
|
background: 'var(--bg-2)',
|
||||||
|
border: '1px solid var(--line)',
|
||||||
|
borderRadius: 'var(--r-1)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||||
|
<span style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-mute)' }}>
|
||||||
|
{s.label}
|
||||||
|
{s.enabled ? ` · ${s.axisCount} ax` : ''}
|
||||||
|
</span>
|
||||||
|
{depth !== 'peek' && (
|
||||||
|
<span style={{ fontSize: 9, color: STATUS_TONE[s.status.state] ?? 'var(--fg-dim)' }}>
|
||||||
|
{s.status.message}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Switch checked={s.enabled} onChange={(v) => inp.setEnabled(s.kind, v)} label="" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
{depth !== 'peek' && (
|
{depth !== 'peek' && (
|
||||||
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
|
<>
|
||||||
v1 browser is fixed-2-input ({`MLP<2,…>`}). The modular N×M input matrix, per-axis pipeline
|
{/* ---- Gamepad config ---- */}
|
||||||
(deadzone → zoom → curve → smoothing → momentum) and MIDI / gamepad sources are owned by
|
{inp.sources.find((s) => s.kind === 'gamepad')?.enabled && (
|
||||||
workstream F (inputs-spec); this drawer fixes the dock shape only.
|
<>
|
||||||
</p>
|
<SectionLabel>Gamepad · sticks</SectionLabel>
|
||||||
|
<Segmented
|
||||||
|
value={inp.gamepadStickMode}
|
||||||
|
onChange={inp.setGamepadStickMode}
|
||||||
|
options={[
|
||||||
|
{ value: 'single', label: 'Single (2 ax)' },
|
||||||
|
{ value: 'double', label: 'Double (4 ax)' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ---- MIDI learn-map ---- */}
|
||||||
|
{inp.sources.find((s) => s.kind === 'midi')?.enabled && (
|
||||||
|
<>
|
||||||
|
<SectionLabel>MIDI · learn-map</SectionLabel>
|
||||||
|
<div style={{ display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant={inp.midiLearnArmed ? 'primary' : 'secondary'}
|
||||||
|
onClick={() => inp.armMidiLearn(!inp.midiLearnArmed)}
|
||||||
|
>
|
||||||
|
{inp.midiLearnArmed ? 'learning… (move a control)' : 'Learn axis'}
|
||||||
|
</Button>
|
||||||
|
{inp.midiBindings.length > 0 && (
|
||||||
|
<Button size="sm" variant="secondary" onClick={inp.clearMidiBindings}>
|
||||||
|
Clear all
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{inp.midiBindings.length === 0 ? (
|
||||||
|
<p style={{ fontSize: 9, color: 'var(--fg-dim)', margin: 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).
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||||
|
{inp.midiBindings.map((b, i) => (
|
||||||
|
<div
|
||||||
|
key={`${b.kind}-${b.number}-${b.channel}`}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
fontSize: 'var(--fs-xs)',
|
||||||
|
color: 'var(--fg-mute)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{b.label} · {b.value.toFixed(2)}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => inp.clearMidiBinding(i)}
|
||||||
|
style={{
|
||||||
|
background: 'transparent',
|
||||||
|
border: 0,
|
||||||
|
color: 'var(--danger)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: 'var(--fs-xs)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{inp.midiInputs.length > 0 && (
|
||||||
|
<p style={{ fontSize: 9, color: 'var(--fg-dim)', margin: 0 }}>
|
||||||
|
Listening on: {inp.midiInputs.map((p) => p.name).join(', ')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ---- Channel layout ---- */}
|
||||||
|
<SectionLabel>Channel layout</SectionLabel>
|
||||||
|
{inp.channelLayout.length === 0 ? (
|
||||||
|
<p style={{ fontSize: 9, color: 'var(--fg-dim)', margin: 0 }}>
|
||||||
|
No active axes. Enable a source above.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
|
||||||
|
{inp.channelLayout.map((c, i) => (
|
||||||
|
<Chip key={i}>
|
||||||
|
{i}: {c.source}·{c.label}
|
||||||
|
</Chip>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<p style={{ fontSize: 9, color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
|
||||||
|
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).
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import type { MFMode, MFParam } from './model';
|
||||||
import type { BackendId } from '../dock/output-state';
|
import type { BackendId } from '../dock/output-state';
|
||||||
import type { FeedbackMode } from '../engine/types';
|
import type { FeedbackMode } from '../engine/types';
|
||||||
import type { BackendStatus } from '../backends/backend';
|
import type { BackendStatus } from '../backends/backend';
|
||||||
|
import type { UseInputLayer } from '../inputs';
|
||||||
|
|
||||||
/** The two product feedback modes (dock-spec §1.1; rl-feedback-design §0). */
|
/** The two product feedback modes (dock-spec §1.1; rl-feedback-design §0). */
|
||||||
export type FeedbackModeUI = 'explore-and-place' | 'geometric-dislike';
|
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 plotted at the input location where each feedback was given. */
|
||||||
markers: FeedbackMarker[];
|
markers: FeedbackMarker[];
|
||||||
|
|
||||||
|
// ---- Modular input layer (workstream F; inputs-spec) ----
|
||||||
|
/** The composed input layer: source enable/config/status + channel layout. */
|
||||||
|
inputs: UseInputLayer;
|
||||||
|
|
||||||
health: number;
|
health: number;
|
||||||
gradient: number[];
|
gradient: number[];
|
||||||
gradientStatus: string[];
|
gradientStatus: string[];
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue