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 { 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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
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 (
|
||||
<>
|
||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
||||
<Badge tone="info">{src}</Badge>
|
||||
<Chip>2 inputs</Chip>
|
||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<Badge tone="info">
|
||||
{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>
|
||||
<SectionLabel>Source</SectionLabel>
|
||||
<Segmented
|
||||
value={ctx.mode.input}
|
||||
onChange={() => {
|
||||
/* 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' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<SectionLabel>Sources</SectionLabel>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
{inp.sources.map((s) => (
|
||||
<div
|
||||
key={s.kind}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
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' && (
|
||||
<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
|
||||
(deadzone → zoom → curve → smoothing → momentum) and MIDI / gamepad sources are owned by
|
||||
workstream F (inputs-spec); this drawer fixes the dock shape only.
|
||||
</p>
|
||||
<>
|
||||
{/* ---- Gamepad config ---- */}
|
||||
{inp.sources.find((s) => s.kind === 'gamepad')?.enabled && (
|
||||
<>
|
||||
<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 { 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[];
|
||||
|
|
|
|||
Loading…
Reference in a new issue