memlnaut-nisps/manifold/src/console/Drawers.tsx

831 lines
32 KiB
TypeScript
Raw Normal View History

/**
* Console the FIVE real dock drawers (operator dock restructure). Each renderer
* takes (ctx, depth); what shows is gated by depth (condensed | expanded):
*
* learn Learning : feedback-mode selector, solo/arm chooser, live params
* inputs Inputs : input source
* route Outputs : per-output control matrix for the ACTIVE mode/backend,
* a Mode-specific config section, and (Editor mode) the
* MEMLNaut serial panel. The old separate "Synth" and
* "Particle/Visual" drawers are REMOVED their config now
* lives here under the active Mode (TOP dock selector).
* settings Settings : icon style + input-map shape (settings-store)
* help Help : keymap + the loop explanation
*
* The TOP dock selector ("Mode") chooses the active OUTPUT backend/target; this
* drawer renders whatever that backend needs.
*
* Engine wiring: the feedback-mode pill controller.setMode; the arm flags
* engine.feedback.setFocus; per-output rows write the shared MFParam store.
* Where engine support does not yet exist the UI + state are wired and a TODO
* references the relevant spec no faked engine behaviour.
*/
import type { ReactNode } from 'react';
import { Badge, Button, PillToggle, Slider, Switch } from '../primitives';
import type { ConsoleCtx, DrawerDepth, DrawerKey, FeedbackModeUI, SoloMode } from './types';
feat(manifold): MIDI + game controller inputs; widen ML net to N-D Wire the modular input layer into the Console and reshape the browser engine so input axes are genuine independent dimensions. Inputs (manifold/src/inputs/): - gamepad-source: emit press+release edges with standard-mapping labels (enables hold-and-move); single/double-stick already present. - midi-input-source: single-device selection + batch "MIDI Learn" (every CC swept while armed becomes an axis); notes stay discrete. - input-layer: compose() forwards each axis 1:1 (no mean-blend); add onReducedInput so the manifold tracks gamepad/MIDI position. - types: InputAction.phase, InputMode. Console (manifold/src/console/): - ConsoleApp: bind gamepad buttons to verdicts (RB up / LB down / X randomise / Y nudge / B undo / A-hold reposition); mirror composed position onto the manifold. - Drawers: rebuilt Inputs drawer (source picker, gamepad legend, MIDI device picker + batch-learn flow, learned-control meters). Engine (nisps/wasm, manifold/src/engine): - DefaultMLP widened MLP<2,..> -> MLP<32,..> (32 = MAX_AXES); each active axis gets a dedicated slot, unused slots held at 0 (inert). Rebuilt nisps.wasm (playground + manifold). - spine/engine-api: setInputs writes the full N-D vector (was dropping arr[2+]); primary pair keeps the 2-D pipeline; process() re-ticks the whole vector via spine.reprocess(). Tests: - parity_check/parity_wasm: ParityMLP -> 32 inputs, widen example bufs. - CMakeLists: build parity binary with -ffp-contract=off so native matches FMA-free WASM (training amplified the gap past 1e-5). Inputs dock is still an exclusive picker; mixing toggles, reshape modal, and the >2-D slider view (inputs-spec.md) are groundwork-laid but not yet wired. See docs/redesign/midi-gamepad-inputs-worklog.md.
2026-06-28 21:05:30 +02:00
import type { InputMode } from '../inputs';
import { OutputControlRow } from '../dock/OutputControlRow';
import { OutputsBackendConfig, BackendStatusChip } from '../dock/OutputsBackendConfig';
import { shapeValues } from './model';
import { outputModeDescriptor } from './output-mode';
import { useSettings, unfocusedIconCss } from '../settings/settings-store';
import type { UnfocusedIconColour, InputMapMode } from '../settings/settings-store';
import { EditorPanel } from '../serial/EditorPanel';
feat: curve truth, DriverConfig, real telemetry, engine benchmark Four items from one workflow, committed together because their build and CI wiring genuinely interleaves — nisps/CMakeLists.txt, run-all-tests.sh and ci.yml each carry hunks from two of them, and the stage renumbering (1/5 -> 1/6) touches every line. Splitting would produce commits that do not build, which is worse than a commit that does four things and says so. S26 part 2 — the curve declaration now matches reality. params[].curve stays the mode-wide DEFAULT; a voice_spaces entry may now be {name, curve_overrides} declaring only the slots where THAT voice space deviates. The 6 modes with one voice space are byte-identical. The values were derived MECHANICALLY by a new codegen/curve-audit.ts that models the four idioms a p[N]*p[N] regex misses (alias form, memlcelium's implicit-counter sq() lambda, loop-generated indices, smooth_params_), inlines helpers, and RAISES rather than guessing when it cannot reduce an expression. A drift gate cross-checks 1179 (voice space x param) slots against engine source on every run and was proved to fail loudly on three drift classes. Application stays in the engine: nisps/engines, nisps/pipeline and nisps/core are untouched, generated output is pure insertion (755 insertions, 0 deletions), and the rebuilt nisps.wasm was byte-identical. S4 / 7.2 — firmware reads the active mode's driver config at mode start, and mic/line is real. My brief assumed the engine owns this; the code disagreed and the code was right. sound_analysis_midi's EngineT is NoOpEngine — the mic lives on a separately-composed AnalysisEngine member — so engine-level wiring would have compiled, passed every gate, and left the one mic mode on line input. Hence a mode-level seam defaulting to engine().driver_config(). Separately, DriverConfig's defaults (line_level 0, output_volume 1.0) had drifted from memllib's actual 3/0.8 because nothing had ever read them; wiring them as-is would have made every silent mode louder and its line input maximally insensitive — a behaviour change disguised as plumbing. Now pinned by a test. Also: GetSysClockSpeed() panic()s on unsupported sample rates and runs on the first line of setup(), so sample_rate needed a fallback ahead of clock setup. CI's firmware env list gains soundanalysismidi — it is the only mic variant and nothing else compiles that path. Plan 5e — telemetry is real. A loss_history C-API entry across the full 5-layer chain lets the browser read the per-iteration loss the core already records. The audit named one fabrication site; there were two — wasm-iml.ts's synchronous train() published lossHistory: [loss] as well. A third, ctx.loss, was not merely dead but actively synthetic (fallbacks of prev * 0.82 and a literal 0.5, rendered by nothing) and is deleted. The firmware buffer stays untouched, per the L25 call. EngineApi.lossHistory() reads spine state rather than the MLP handle, because trainAsync() fits on the worker's mirror net and the handle would give a subtly-wrong second answer. Plan 5f — engine throughput is measurable. One source compiled twice (CMake natively, emcc for WASM) so the targets compare directly and no WASM export is added. Sequencers are driven into a working state, and every row prints its own working-state evidence so a number produced by an idle engine is visible rather than plausible. Reports, never asserts: a wall-clock threshold on shared hardware is meaningless or flaky, same call as the firmware size job. ALIGNMENT: the telemetry defect is deleted (built, not deferred); the performance defect is rewritten to what is actually left — these are HOST numbers, and nothing measures the RP2350 at 150 MHz, which is the target the mission's constraint is about. Q4 (memllib ownership) and Q5 (legacy feedback modes) are closed. Corrections to my own earlier claims, both found by agents contradicting the brief: manifold/ONBOARDING.md was NOT "now accurate" — its primitives list still named five deleted primitives and cited a seededGradient() that does not exist. And the parity harness misses the sequencer engines because it runs 128 frames while their sequencers evaluate every 400-500 samples, NOT because all-params-0.5 fails to trigger them (it does trigger: 0.5 maps to ratio 2, firing three times per bar). The fix is a longer window, not different params. Gates: run-all-tests.sh ALL GREEN — 4/4 ctest, parity PASS, lint clean, curve drift 1179 slots ok, 39 e2e (was 33). Firmware: 5 envs built including the mic variant.
2026-07-21 22:02:23 +02:00
import { TrainingHealth } from './TrainingHealth';
import {
LearningIcon,
InputsIcon,
OutputsIcon,
SettingsIcon,
HelpIcon,
} from './icons';
function Chip({ children, tone }: { children: ReactNode; tone?: string }) {
return (
<span
style={{
fontSize: 10,
fontFamily: 'var(--font-mono)',
textTransform: 'uppercase',
letterSpacing: '0.06em',
color: tone || 'var(--fg-mute)',
background: 'var(--bg-2)',
border: '1px solid var(--line)',
borderRadius: 'var(--r-pill)',
padding: '2px 8px',
}}
>
{children}
</span>
);
}
function SectionLabel({ children }: { children: ReactNode }) {
return (
<div
style={{
fontSize: 10,
color: 'var(--fg-dim)',
textTransform: 'uppercase',
letterSpacing: '0.1em',
marginTop: 'var(--sp-2)',
}}
>
{children}
</div>
);
}
/** A 2+ segment selector pill that drives a typed value. */
function Segmented<T extends string>({
value,
onChange,
options,
}: {
value: T;
onChange: (v: T) => void;
options: { value: T; label: string }[];
}) {
return (
<div
style={{
display: 'inline-flex',
background: 'var(--bg-2)',
border: '1px solid var(--line)',
borderRadius: 'var(--r-pill)',
padding: 2,
gap: 2,
flexWrap: 'wrap',
}}
>
{options.map((o) => {
const on = value === o.value;
return (
<button
key={o.value}
type="button"
onClick={() => onChange(o.value)}
style={{
background: on ? 'var(--accent)' : 'transparent',
color: on ? 'var(--bg)' : 'var(--fg-mute)',
border: 0,
borderRadius: 'var(--r-pill)',
padding: '5px 12px',
fontFamily: 'var(--font-mono)',
fontSize: 'var(--fs-xs)',
letterSpacing: '0.04em',
cursor: 'pointer',
}}
>
{o.label}
</button>
);
})}
</div>
);
}
// ===========================================================================
// 1. LEARNING-BEHAVIOUR (dock-spec §1; rl-feedback-design)
// ===========================================================================
const FEEDBACK_OPTS: { value: FeedbackModeUI; label: string }[] = [
{ value: 'geometric-dislike', label: 'Push away' },
{ value: 'explore-and-place', label: 'Explore & place' },
];
const FEEDBACK_DESC: Record<FeedbackModeUI, string> = {
'geometric-dislike':
'Down carves the current sound away from what you like — directed repulsion (Mode 1).',
'explore-and-place':
'Down re-rolls the whole net into a scratchpad you audition; + places a liked sound (Mode 2).',
};
refactor(manifold): delete the dead console UI stratum Phase 1 group 5 (S15, S16, S18, S19, L22, L23, L20, L21, L1 delete-half). - S15: the four-way focus/altitude system. setFocus was never called anywhere, so only the 'composite' branch was reachable. Deleted SplitStage, ReadoutStrip, InputMini, AltitudeNav, CompactAxis, the UI Focus type/prop, the focus branches, stripPinned and the vacuous keyboard gates. MiniMeters kept; engine.feedback.setFocus (a different, live thing) untouched. - S16 + L1: the decorative stratum that rendered real-looking controls driving nothing — A/B machinery, the fake seed, seededGradient + weightsRevision, snapshots, master volume, bpm, and the learningRate/decay/tame/spreadLevel sliders with their Drawers rows. Each was confirmed self-referential first. NOTE a real behaviour change falls out of dropping `snapshots`: Undo outside an active explore-and-place session used to pop a UI-only snapshot that restored noiseCap/seed. It is now simply inactive unless a genuine core-backed scratchpad undo exists. Geometric-dislike mode never had a real undo primitive in the core, so only the fake path is gone. - S18: BackendAdvanced.tsx and its Drawers block. It self-described as a duplicate of the inline OutputsBackendConfig editor and BOTH rendered in the same expanded drawer. OutputsBackendConfig already covers every backend. - S19: pruned ConsoleCtx to the fields Dock/Drawers/OutputsBackendConfig actually read; deleted the Axes type + axes/setAxis and the preset/setPreset/offsetActive chain (permanently 'Sculpt'/false). KEPT ctx.modes and ctx.setModeId despite having no reader today — the Phase 5 instrument picker (§7.6, adopted) is built on exactly that plumbing. - L22: the 5 dead primitives (Panel, StatusLine, ControlAxis, CurvePlot, Sparkline) and their barrel exports, plus the now-dead .mf-axis-input CSS. - L23: OutputControl/toOutputControl, ModeIconComponent and the BACKENDS catalogue; Drawers now reads modeDesc.label/description from OUTPUT_MODES, the surviving single catalogue. - L20: the solo-mode selector's two unimplemented options no longer pretend to be selectable. - L21: FeedbackController vestiges — seed/undoDepth options, maxUndo, and six ControllerEngine members nothing called (the finding named three; the other three are used on the real EngineApi by debug/probe.ts, a different interface, so removing them from ControllerEngine is safe). Gates: run-all-tests.sh ALL GREEN (typecheck, 33 Playwright specs).
2026-07-21 12:49:25 +02:00
/**
* Solo behaviour. `mask-gradients` is the only variant the core actually
* implements `soloMode` has no other observable effect today (the other two
* options need the C API's `train_masked` step). Rendered as a fixed,
* non-selectable label rather than a picker that offers dead choices
* (simplification audit L20).
*/
const SOLO_OPTS: { value: SoloMode; label: string }[] = [
{ value: 'mask-gradients', label: 'Mask gradients' },
{ value: 'zero-loss', label: 'Zero loss' },
{ value: 'dont-care', label: "Don't-care mask" },
];
const SOLO_DESC: Record<SoloMode, string> = {
'mask-gradients': 'Column-freeze (default) — only the armed output moves; the rest stay bit-identical.',
'zero-loss': 'Expressive, but armed and unarmed outputs share hidden weights, so others can drift.',
'dont-care': 'Each example stores a per-output mask so stale labels never pull unarmed outputs.',
};
function LearningDrawer(ctx: ConsoleCtx, depth: DrawerDepth) {
return (
<>
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', alignItems: 'center' }}>
<Chip tone="var(--accent)">{FEEDBACK_OPTS.find((o) => o.value === ctx.feedbackMode)?.label}</Chip>
<Chip>arm: {ctx.armedCount ? `${ctx.armedCount} output${ctx.armedCount > 1 ? 's' : ''}` : 'all'}</Chip>
{ctx.exploring && <Chip tone="var(--accent-2)">exploring</Chip>}
{ctx.learningPaused && <Chip tone="var(--warn)">learning paused</Chip>}
</div>
<SectionLabel>Down action · feedback mode</SectionLabel>
<Segmented value={ctx.feedbackMode} onChange={ctx.setFeedbackMode} options={FEEDBACK_OPTS} />
{depth === 'expanded' && (
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
{FEEDBACK_DESC[ctx.feedbackMode]}
</p>
)}
<SectionLabel>Exploration</SectionLabel>
<div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
<Button
size="sm"
variant="secondary"
active={ctx.joltActive}
aria-label="Jolt — hold to morph the network's weights, release to freeze"
aria-pressed={ctx.joltActive}
onPointerDown={(e) => {
e.preventDefault();
ctx.onJoltPress();
}}
onPointerUp={ctx.onJoltRelease}
onPointerLeave={ctx.onJoltRelease}
onPointerCancel={ctx.onJoltRelease}
style={{ touchAction: 'none', userSelect: 'none' }}
>
Jolt {ctx.joltActive ? '(morphing…)' : '(hold)'}
</Button>
<span style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)' }}>
hold to morph the whole net live · release to freeze
</span>
</div>
<Slider
label="explore · output wander"
value={ctx.exploreIntensity}
min={0}
max={1}
step={0.01}
onChange={ctx.setExploreIntensity}
/>
{depth === 'expanded' && (
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
Explore adds a slow random walk (Ornstein-Uhlenbeck) on the outputs so the sound roams;
likes and dislikes registered mid-wander steer the net toward what you want. 0 = off. Jolt
(firmware TogB1) and Explore (RVX1) drive the same core gestures as the MEMLNaut hardware.
</p>
)}
<SectionLabel>Recorded examples</SectionLabel>
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<Chip>
{ctx.datasetCount} example{ctx.datasetCount === 1 ? '' : 's'}
</Chip>
<Button size="sm" variant="secondary" onClick={ctx.onClear}>
Clear
</Button>
<span style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)' }}>
forget every example & wipe the on-map marks
</span>
</div>
{depth === 'expanded' && (
<>
<SectionLabel>Solo / arm scope</SectionLabel>
<div style={{ display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap' }}>
<Button size="sm" variant={ctx.armedCount ? 'secondary' : 'primary'} onClick={ctx.clearArmed}>
Arm all
</Button>
<span style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)' }}>
{ctx.armedCount
? `${ctx.armedCount} armed — arm with the S button on each output row`
: 'every live output learns'}
</span>
</div>
<SectionLabel>Solo behaviour</SectionLabel>
refactor(manifold): delete the dead console UI stratum Phase 1 group 5 (S15, S16, S18, S19, L22, L23, L20, L21, L1 delete-half). - S15: the four-way focus/altitude system. setFocus was never called anywhere, so only the 'composite' branch was reachable. Deleted SplitStage, ReadoutStrip, InputMini, AltitudeNav, CompactAxis, the UI Focus type/prop, the focus branches, stripPinned and the vacuous keyboard gates. MiniMeters kept; engine.feedback.setFocus (a different, live thing) untouched. - S16 + L1: the decorative stratum that rendered real-looking controls driving nothing — A/B machinery, the fake seed, seededGradient + weightsRevision, snapshots, master volume, bpm, and the learningRate/decay/tame/spreadLevel sliders with their Drawers rows. Each was confirmed self-referential first. NOTE a real behaviour change falls out of dropping `snapshots`: Undo outside an active explore-and-place session used to pop a UI-only snapshot that restored noiseCap/seed. It is now simply inactive unless a genuine core-backed scratchpad undo exists. Geometric-dislike mode never had a real undo primitive in the core, so only the fake path is gone. - S18: BackendAdvanced.tsx and its Drawers block. It self-described as a duplicate of the inline OutputsBackendConfig editor and BOTH rendered in the same expanded drawer. OutputsBackendConfig already covers every backend. - S19: pruned ConsoleCtx to the fields Dock/Drawers/OutputsBackendConfig actually read; deleted the Axes type + axes/setAxis and the preset/setPreset/offsetActive chain (permanently 'Sculpt'/false). KEPT ctx.modes and ctx.setModeId despite having no reader today — the Phase 5 instrument picker (§7.6, adopted) is built on exactly that plumbing. - L22: the 5 dead primitives (Panel, StatusLine, ControlAxis, CurvePlot, Sparkline) and their barrel exports, plus the now-dead .mf-axis-input CSS. - L23: OutputControl/toOutputControl, ModeIconComponent and the BACKENDS catalogue; Drawers now reads modeDesc.label/description from OUTPUT_MODES, the surviving single catalogue. - L20: the solo-mode selector's two unimplemented options no longer pretend to be selectable. - L21: FeedbackController vestiges — seed/undoDepth options, maxUndo, and six ControllerEngine members nothing called (the finding named three; the other three are used on the real EngineApi by debug/probe.ts, a different interface, so removing them from ControllerEngine is safe). Gates: run-all-tests.sh ALL GREEN (typecheck, 33 Playwright specs).
2026-07-21 12:49:25 +02:00
<Chip tone="var(--accent)">{SOLO_OPTS.find((o) => o.value === ctx.soloMode)?.label}</Chip>
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
{SOLO_DESC[ctx.soloMode]} Solo only freezes the rest as far as a shared network allows.
refactor(manifold): delete the dead console UI stratum Phase 1 group 5 (S15, S16, S18, S19, L22, L23, L20, L21, L1 delete-half). - S15: the four-way focus/altitude system. setFocus was never called anywhere, so only the 'composite' branch was reachable. Deleted SplitStage, ReadoutStrip, InputMini, AltitudeNav, CompactAxis, the UI Focus type/prop, the focus branches, stripPinned and the vacuous keyboard gates. MiniMeters kept; engine.feedback.setFocus (a different, live thing) untouched. - S16 + L1: the decorative stratum that rendered real-looking controls driving nothing — A/B machinery, the fake seed, seededGradient + weightsRevision, snapshots, master volume, bpm, and the learningRate/decay/tame/spreadLevel sliders with their Drawers rows. Each was confirmed self-referential first. NOTE a real behaviour change falls out of dropping `snapshots`: Undo outside an active explore-and-place session used to pop a UI-only snapshot that restored noiseCap/seed. It is now simply inactive unless a genuine core-backed scratchpad undo exists. Geometric-dislike mode never had a real undo primitive in the core, so only the fake path is gone. - S18: BackendAdvanced.tsx and its Drawers block. It self-described as a duplicate of the inline OutputsBackendConfig editor and BOTH rendered in the same expanded drawer. OutputsBackendConfig already covers every backend. - S19: pruned ConsoleCtx to the fields Dock/Drawers/OutputsBackendConfig actually read; deleted the Axes type + axes/setAxis and the preset/setPreset/offsetActive chain (permanently 'Sculpt'/false). KEPT ctx.modes and ctx.setModeId despite having no reader today — the Phase 5 instrument picker (§7.6, adopted) is built on exactly that plumbing. - L22: the 5 dead primitives (Panel, StatusLine, ControlAxis, CurvePlot, Sparkline) and their barrel exports, plus the now-dead .mf-axis-input CSS. - L23: OutputControl/toOutputControl, ModeIconComponent and the BACKENDS catalogue; Drawers now reads modeDesc.label/description from OUTPUT_MODES, the surviving single catalogue. - L20: the solo-mode selector's two unimplemented options no longer pretend to be selectable. - L21: FeedbackController vestiges — seed/undoDepth options, maxUndo, and six ControllerEngine members nothing called (the finding named three; the other three are used on the real EngineApi by debug/probe.ts, a different interface, so removing them from ControllerEngine is safe). Gates: run-all-tests.sh ALL GREEN (typecheck, 33 Playwright specs).
2026-07-21 12:49:25 +02:00
The other two behaviours need real core support (`train_masked`) and aren't selectable yet.
</p>
<SectionLabel>Live training params</SectionLabel>
<Slider label="noise cap" value={ctx.noiseCap} min={0} max={0.5} step={0.01} onChange={ctx.setNoiseCap} />
</>
)}
{depth === 'expanded' && (
<>
<SectionLabel>Feedback lab</SectionLabel>
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
State machine: idle exploring commit / cancel. While Explore & place is exploring,
training is paused and the joystick auditions a random scratchpad net; + commits a placed
anchor and restores the real net.
</p>
<Switch checked={ctx.spread} onChange={ctx.setSpread} label="Xavier (centered) weight regime" />
feat: curve truth, DriverConfig, real telemetry, engine benchmark Four items from one workflow, committed together because their build and CI wiring genuinely interleaves — nisps/CMakeLists.txt, run-all-tests.sh and ci.yml each carry hunks from two of them, and the stage renumbering (1/5 -> 1/6) touches every line. Splitting would produce commits that do not build, which is worse than a commit that does four things and says so. S26 part 2 — the curve declaration now matches reality. params[].curve stays the mode-wide DEFAULT; a voice_spaces entry may now be {name, curve_overrides} declaring only the slots where THAT voice space deviates. The 6 modes with one voice space are byte-identical. The values were derived MECHANICALLY by a new codegen/curve-audit.ts that models the four idioms a p[N]*p[N] regex misses (alias form, memlcelium's implicit-counter sq() lambda, loop-generated indices, smooth_params_), inlines helpers, and RAISES rather than guessing when it cannot reduce an expression. A drift gate cross-checks 1179 (voice space x param) slots against engine source on every run and was proved to fail loudly on three drift classes. Application stays in the engine: nisps/engines, nisps/pipeline and nisps/core are untouched, generated output is pure insertion (755 insertions, 0 deletions), and the rebuilt nisps.wasm was byte-identical. S4 / 7.2 — firmware reads the active mode's driver config at mode start, and mic/line is real. My brief assumed the engine owns this; the code disagreed and the code was right. sound_analysis_midi's EngineT is NoOpEngine — the mic lives on a separately-composed AnalysisEngine member — so engine-level wiring would have compiled, passed every gate, and left the one mic mode on line input. Hence a mode-level seam defaulting to engine().driver_config(). Separately, DriverConfig's defaults (line_level 0, output_volume 1.0) had drifted from memllib's actual 3/0.8 because nothing had ever read them; wiring them as-is would have made every silent mode louder and its line input maximally insensitive — a behaviour change disguised as plumbing. Now pinned by a test. Also: GetSysClockSpeed() panic()s on unsupported sample rates and runs on the first line of setup(), so sample_rate needed a fallback ahead of clock setup. CI's firmware env list gains soundanalysismidi — it is the only mic variant and nothing else compiles that path. Plan 5e — telemetry is real. A loss_history C-API entry across the full 5-layer chain lets the browser read the per-iteration loss the core already records. The audit named one fabrication site; there were two — wasm-iml.ts's synchronous train() published lossHistory: [loss] as well. A third, ctx.loss, was not merely dead but actively synthetic (fallbacks of prev * 0.82 and a literal 0.5, rendered by nothing) and is deleted. The firmware buffer stays untouched, per the L25 call. EngineApi.lossHistory() reads spine state rather than the MLP handle, because trainAsync() fits on the worker's mirror net and the handle would give a subtly-wrong second answer. Plan 5f — engine throughput is measurable. One source compiled twice (CMake natively, emcc for WASM) so the targets compare directly and no WASM export is added. Sequencers are driven into a working state, and every row prints its own working-state evidence so a number produced by an idle engine is visible rather than plausible. Reports, never asserts: a wall-clock threshold on shared hardware is meaningless or flaky, same call as the firmware size job. ALIGNMENT: the telemetry defect is deleted (built, not deferred); the performance defect is rewritten to what is actually left — these are HOST numbers, and nothing measures the RP2350 at 150 MHz, which is the target the mission's constraint is about. Q4 (memllib ownership) and Q5 (legacy feedback modes) are closed. Corrections to my own earlier claims, both found by agents contradicting the brief: manifold/ONBOARDING.md was NOT "now accurate" — its primitives list still named five deleted primitives and cited a seededGradient() that does not exist. And the parity harness misses the sequencer engines because it runs 128 frames while their sequencers evaluate every 400-500 samples, NOT because all-params-0.5 fails to trigger them (it does trigger: 0.5 maps to ratio 2, firing three times per bar). The fix is a longer window, not different params. Gates: run-all-tests.sh ALL GREEN — 4/4 ctest, parity PASS, lint clean, curve drift 1179 slots ok, 39 e2e (was 33). Firmware: 5 envs built including the mic variant.
2026-07-21 22:02:23 +02:00
<SectionLabel>Training health</SectionLabel>
<TrainingHealth />
</>
)}
</>
);
}
// ===========================================================================
// 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)',
};
feat(manifold): MIDI + game controller inputs; widen ML net to N-D Wire the modular input layer into the Console and reshape the browser engine so input axes are genuine independent dimensions. Inputs (manifold/src/inputs/): - gamepad-source: emit press+release edges with standard-mapping labels (enables hold-and-move); single/double-stick already present. - midi-input-source: single-device selection + batch "MIDI Learn" (every CC swept while armed becomes an axis); notes stay discrete. - input-layer: compose() forwards each axis 1:1 (no mean-blend); add onReducedInput so the manifold tracks gamepad/MIDI position. - types: InputAction.phase, InputMode. Console (manifold/src/console/): - ConsoleApp: bind gamepad buttons to verdicts (RB up / LB down / X randomise / Y nudge / B undo / A-hold reposition); mirror composed position onto the manifold. - Drawers: rebuilt Inputs drawer (source picker, gamepad legend, MIDI device picker + batch-learn flow, learned-control meters). Engine (nisps/wasm, manifold/src/engine): - DefaultMLP widened MLP<2,..> -> MLP<32,..> (32 = MAX_AXES); each active axis gets a dedicated slot, unused slots held at 0 (inert). Rebuilt nisps.wasm (playground + manifold). - spine/engine-api: setInputs writes the full N-D vector (was dropping arr[2+]); primary pair keeps the 2-D pipeline; process() re-ticks the whole vector via spine.reprocess(). Tests: - parity_check/parity_wasm: ParityMLP -> 32 inputs, widen example bufs. - CMakeLists: build parity binary with -ffp-contract=off so native matches FMA-free WASM (training amplified the gap past 1e-5). Inputs dock is still an exclusive picker; mixing toggles, reshape modal, and the >2-D slider view (inputs-spec.md) are groundwork-laid but not yet wired. See docs/redesign/midi-gamepad-inputs-worklog.md.
2026-06-28 21:05:30 +02:00
const INPUT_MODE_OPTS: { value: InputMode; label: string }[] = [
{ value: 'internal', label: 'Internal' },
{ value: 'gamepad', label: 'Game Controller' },
{ value: 'midi', label: 'MIDI' },
];
/** Standard-mapping gamepad button → verdict legend (mirrors ConsoleApp). */
const GAMEPAD_LEGEND: { btn: string; action: string }[] = [
{ btn: 'RB', action: 'Up · positive feedback' },
{ btn: 'LB', action: 'Down · negative feedback' },
{ btn: 'X', action: 'Randomise' },
{ btn: 'Y', action: 'Nudge' },
{ btn: 'B', action: 'Undo' },
{ btn: 'A (hold)', action: 'Reposition — hold, move stick, release to place' },
];
/**
* Read-only INPUT meter shows a learned MIDI control's live value. Deliberately
* styled apart from the output Sliders (which are orange, interactive thumbs):
* these are inset bars on the secondary accent with an "in" tag, so the user can
* see at a glance that these feed the net rather than being driven by it.
*/
function MidiInputMeter({ label, value, onClear }: { label: string; value: number; onClear: () => void }) {
const pct = Math.max(0, Math.min(1, value));
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span
style={{
fontSize: 9,
fontFamily: 'var(--font-mono)',
color: 'var(--accent-2)',
textTransform: 'uppercase',
letterSpacing: '0.06em',
minWidth: 70,
}}
>
{label}
</span>
<div
style={{
position: 'relative',
flex: 1,
height: 8,
background: 'var(--bg-2)',
border: '1px solid var(--line)',
borderLeft: '2px solid var(--accent-2)',
borderRadius: 'var(--r-1)',
overflow: 'hidden',
}}
>
<div
style={{
position: 'absolute',
inset: 0,
width: `${pct * 100}%`,
background: 'var(--accent-2)',
opacity: 0.55,
}}
/>
</div>
<span
style={{
fontSize: 9,
fontFamily: 'var(--font-mono)',
color: 'var(--fg-mute)',
minWidth: 28,
textAlign: 'right',
}}
>
{value.toFixed(2)}
</span>
<button
type="button"
onClick={onClear}
aria-label={`Remove ${label}`}
style={{ background: 'transparent', border: 0, color: 'var(--danger)', cursor: 'pointer', fontSize: 'var(--fs-xs)' }}
>
</button>
</div>
);
}
function InputsDrawer(ctx: ConsoleCtx, depth: DrawerDepth) {
const inp = ctx.inputs;
// The net is runtime-shaped (P2): each active axis drives its OWN input slot
// 1:1. A mismatch means the net is over-provisioned (axes < slots, extra slots
// zero-padded) or over capacity (axes > slots, extras dropped) — the reshape
// offer resolves either.
const inputMismatch = inp.axisCount > 0 && inp.axisCount !== inp.engineInputSize;
feat(manifold): MIDI + game controller inputs; widen ML net to N-D Wire the modular input layer into the Console and reshape the browser engine so input axes are genuine independent dimensions. Inputs (manifold/src/inputs/): - gamepad-source: emit press+release edges with standard-mapping labels (enables hold-and-move); single/double-stick already present. - midi-input-source: single-device selection + batch "MIDI Learn" (every CC swept while armed becomes an axis); notes stay discrete. - input-layer: compose() forwards each axis 1:1 (no mean-blend); add onReducedInput so the manifold tracks gamepad/MIDI position. - types: InputAction.phase, InputMode. Console (manifold/src/console/): - ConsoleApp: bind gamepad buttons to verdicts (RB up / LB down / X randomise / Y nudge / B undo / A-hold reposition); mirror composed position onto the manifold. - Drawers: rebuilt Inputs drawer (source picker, gamepad legend, MIDI device picker + batch-learn flow, learned-control meters). Engine (nisps/wasm, manifold/src/engine): - DefaultMLP widened MLP<2,..> -> MLP<32,..> (32 = MAX_AXES); each active axis gets a dedicated slot, unused slots held at 0 (inert). Rebuilt nisps.wasm (playground + manifold). - spine/engine-api: setInputs writes the full N-D vector (was dropping arr[2+]); primary pair keeps the 2-D pipeline; process() re-ticks the whole vector via spine.reprocess(). Tests: - parity_check/parity_wasm: ParityMLP -> 32 inputs, widen example bufs. - CMakeLists: build parity binary with -ffp-contract=off so native matches FMA-free WASM (training amplified the gap past 1e-5). Inputs dock is still an exclusive picker; mixing toggles, reshape modal, and the >2-D slider view (inputs-spec.md) are groundwork-laid but not yet wired. See docs/redesign/midi-gamepad-inputs-worklog.md.
2026-06-28 21:05:30 +02:00
const active = inp.sources.find((s) => s.enabled);
const modeLabel = INPUT_MODE_OPTS.find((o) => o.value === inp.inputMode)?.label ?? 'Internal';
return (
<>
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', alignItems: 'center' }}>
feat(manifold): MIDI + game controller inputs; widen ML net to N-D Wire the modular input layer into the Console and reshape the browser engine so input axes are genuine independent dimensions. Inputs (manifold/src/inputs/): - gamepad-source: emit press+release edges with standard-mapping labels (enables hold-and-move); single/double-stick already present. - midi-input-source: single-device selection + batch "MIDI Learn" (every CC swept while armed becomes an axis); notes stay discrete. - input-layer: compose() forwards each axis 1:1 (no mean-blend); add onReducedInput so the manifold tracks gamepad/MIDI position. - types: InputAction.phase, InputMode. Console (manifold/src/console/): - ConsoleApp: bind gamepad buttons to verdicts (RB up / LB down / X randomise / Y nudge / B undo / A-hold reposition); mirror composed position onto the manifold. - Drawers: rebuilt Inputs drawer (source picker, gamepad legend, MIDI device picker + batch-learn flow, learned-control meters). Engine (nisps/wasm, manifold/src/engine): - DefaultMLP widened MLP<2,..> -> MLP<32,..> (32 = MAX_AXES); each active axis gets a dedicated slot, unused slots held at 0 (inert). Rebuilt nisps.wasm (playground + manifold). - spine/engine-api: setInputs writes the full N-D vector (was dropping arr[2+]); primary pair keeps the 2-D pipeline; process() re-ticks the whole vector via spine.reprocess(). Tests: - parity_check/parity_wasm: ParityMLP -> 32 inputs, widen example bufs. - CMakeLists: build parity binary with -ffp-contract=off so native matches FMA-free WASM (training amplified the gap past 1e-5). Inputs dock is still an exclusive picker; mixing toggles, reshape modal, and the >2-D slider view (inputs-spec.md) are groundwork-laid but not yet wired. See docs/redesign/midi-gamepad-inputs-worklog.md.
2026-06-28 21:05:30 +02:00
<Badge tone="info">{modeLabel}</Badge>
{inp.inputMode !== 'internal' && <Chip tone="var(--accent)">{inp.axisCount} axes</Chip>}
{inputMismatch && (
<Chip tone="var(--warn)">
{inp.axisCount > inp.engineInputSize
? `${inp.axisCount} axes ${inp.engineInputSize} slots`
: `net: ${inp.engineInputSize}-D`}
</Chip>
)}
feat(manifold): MIDI + game controller inputs; widen ML net to N-D Wire the modular input layer into the Console and reshape the browser engine so input axes are genuine independent dimensions. Inputs (manifold/src/inputs/): - gamepad-source: emit press+release edges with standard-mapping labels (enables hold-and-move); single/double-stick already present. - midi-input-source: single-device selection + batch "MIDI Learn" (every CC swept while armed becomes an axis); notes stay discrete. - input-layer: compose() forwards each axis 1:1 (no mean-blend); add onReducedInput so the manifold tracks gamepad/MIDI position. - types: InputAction.phase, InputMode. Console (manifold/src/console/): - ConsoleApp: bind gamepad buttons to verdicts (RB up / LB down / X randomise / Y nudge / B undo / A-hold reposition); mirror composed position onto the manifold. - Drawers: rebuilt Inputs drawer (source picker, gamepad legend, MIDI device picker + batch-learn flow, learned-control meters). Engine (nisps/wasm, manifold/src/engine): - DefaultMLP widened MLP<2,..> -> MLP<32,..> (32 = MAX_AXES); each active axis gets a dedicated slot, unused slots held at 0 (inert). Rebuilt nisps.wasm (playground + manifold). - spine/engine-api: setInputs writes the full N-D vector (was dropping arr[2+]); primary pair keeps the 2-D pipeline; process() re-ticks the whole vector via spine.reprocess(). Tests: - parity_check/parity_wasm: ParityMLP -> 32 inputs, widen example bufs. - CMakeLists: build parity binary with -ffp-contract=off so native matches FMA-free WASM (training amplified the gap past 1e-5). Inputs dock is still an exclusive picker; mixing toggles, reshape modal, and the >2-D slider view (inputs-spec.md) are groundwork-laid but not yet wired. See docs/redesign/midi-gamepad-inputs-worklog.md.
2026-06-28 21:05:30 +02:00
{active && (
<Chip tone={STATUS_TONE[active.status.state] ?? 'var(--fg-dim)'}>{active.status.state}</Chip>
)}
</div>
feat(manifold): MIDI + game controller inputs; widen ML net to N-D Wire the modular input layer into the Console and reshape the browser engine so input axes are genuine independent dimensions. Inputs (manifold/src/inputs/): - gamepad-source: emit press+release edges with standard-mapping labels (enables hold-and-move); single/double-stick already present. - midi-input-source: single-device selection + batch "MIDI Learn" (every CC swept while armed becomes an axis); notes stay discrete. - input-layer: compose() forwards each axis 1:1 (no mean-blend); add onReducedInput so the manifold tracks gamepad/MIDI position. - types: InputAction.phase, InputMode. Console (manifold/src/console/): - ConsoleApp: bind gamepad buttons to verdicts (RB up / LB down / X randomise / Y nudge / B undo / A-hold reposition); mirror composed position onto the manifold. - Drawers: rebuilt Inputs drawer (source picker, gamepad legend, MIDI device picker + batch-learn flow, learned-control meters). Engine (nisps/wasm, manifold/src/engine): - DefaultMLP widened MLP<2,..> -> MLP<32,..> (32 = MAX_AXES); each active axis gets a dedicated slot, unused slots held at 0 (inert). Rebuilt nisps.wasm (playground + manifold). - spine/engine-api: setInputs writes the full N-D vector (was dropping arr[2+]); primary pair keeps the 2-D pipeline; process() re-ticks the whole vector via spine.reprocess(). Tests: - parity_check/parity_wasm: ParityMLP -> 32 inputs, widen example bufs. - CMakeLists: build parity binary with -ffp-contract=off so native matches FMA-free WASM (training amplified the gap past 1e-5). Inputs dock is still an exclusive picker; mixing toggles, reshape modal, and the >2-D slider view (inputs-spec.md) are groundwork-laid but not yet wired. See docs/redesign/midi-gamepad-inputs-worklog.md.
2026-06-28 21:05:30 +02:00
<SectionLabel>Input source</SectionLabel>
<Segmented value={inp.inputMode} onChange={inp.setInputMode} options={INPUT_MODE_OPTS} />
{active && depth === 'expanded' && (
feat(manifold): MIDI + game controller inputs; widen ML net to N-D Wire the modular input layer into the Console and reshape the browser engine so input axes are genuine independent dimensions. Inputs (manifold/src/inputs/): - gamepad-source: emit press+release edges with standard-mapping labels (enables hold-and-move); single/double-stick already present. - midi-input-source: single-device selection + batch "MIDI Learn" (every CC swept while armed becomes an axis); notes stay discrete. - input-layer: compose() forwards each axis 1:1 (no mean-blend); add onReducedInput so the manifold tracks gamepad/MIDI position. - types: InputAction.phase, InputMode. Console (manifold/src/console/): - ConsoleApp: bind gamepad buttons to verdicts (RB up / LB down / X randomise / Y nudge / B undo / A-hold reposition); mirror composed position onto the manifold. - Drawers: rebuilt Inputs drawer (source picker, gamepad legend, MIDI device picker + batch-learn flow, learned-control meters). Engine (nisps/wasm, manifold/src/engine): - DefaultMLP widened MLP<2,..> -> MLP<32,..> (32 = MAX_AXES); each active axis gets a dedicated slot, unused slots held at 0 (inert). Rebuilt nisps.wasm (playground + manifold). - spine/engine-api: setInputs writes the full N-D vector (was dropping arr[2+]); primary pair keeps the 2-D pipeline; process() re-ticks the whole vector via spine.reprocess(). Tests: - parity_check/parity_wasm: ParityMLP -> 32 inputs, widen example bufs. - CMakeLists: build parity binary with -ffp-contract=off so native matches FMA-free WASM (training amplified the gap past 1e-5). Inputs dock is still an exclusive picker; mixing toggles, reshape modal, and the >2-D slider view (inputs-spec.md) are groundwork-laid but not yet wired. See docs/redesign/midi-gamepad-inputs-worklog.md.
2026-06-28 21:05:30 +02:00
<span style={{ fontSize: 9, color: STATUS_TONE[active.status.state] ?? 'var(--fg-dim)' }}>
{active.status.message}
</span>
)}
feat(manifold): MIDI + game controller inputs; widen ML net to N-D Wire the modular input layer into the Console and reshape the browser engine so input axes are genuine independent dimensions. Inputs (manifold/src/inputs/): - gamepad-source: emit press+release edges with standard-mapping labels (enables hold-and-move); single/double-stick already present. - midi-input-source: single-device selection + batch "MIDI Learn" (every CC swept while armed becomes an axis); notes stay discrete. - input-layer: compose() forwards each axis 1:1 (no mean-blend); add onReducedInput so the manifold tracks gamepad/MIDI position. - types: InputAction.phase, InputMode. Console (manifold/src/console/): - ConsoleApp: bind gamepad buttons to verdicts (RB up / LB down / X randomise / Y nudge / B undo / A-hold reposition); mirror composed position onto the manifold. - Drawers: rebuilt Inputs drawer (source picker, gamepad legend, MIDI device picker + batch-learn flow, learned-control meters). Engine (nisps/wasm, manifold/src/engine): - DefaultMLP widened MLP<2,..> -> MLP<32,..> (32 = MAX_AXES); each active axis gets a dedicated slot, unused slots held at 0 (inert). Rebuilt nisps.wasm (playground + manifold). - spine/engine-api: setInputs writes the full N-D vector (was dropping arr[2+]); primary pair keeps the 2-D pipeline; process() re-ticks the whole vector via spine.reprocess(). Tests: - parity_check/parity_wasm: ParityMLP -> 32 inputs, widen example bufs. - CMakeLists: build parity binary with -ffp-contract=off so native matches FMA-free WASM (training amplified the gap past 1e-5). Inputs dock is still an exclusive picker; mixing toggles, reshape modal, and the >2-D slider view (inputs-spec.md) are groundwork-laid but not yet wired. See docs/redesign/midi-gamepad-inputs-worklog.md.
2026-06-28 21:05:30 +02:00
{/* ---- Internal (XY pad / manifold) ---- */}
{inp.inputMode === 'internal' && depth === 'expanded' && (
feat(manifold): MIDI + game controller inputs; widen ML net to N-D Wire the modular input layer into the Console and reshape the browser engine so input axes are genuine independent dimensions. Inputs (manifold/src/inputs/): - gamepad-source: emit press+release edges with standard-mapping labels (enables hold-and-move); single/double-stick already present. - midi-input-source: single-device selection + batch "MIDI Learn" (every CC swept while armed becomes an axis); notes stay discrete. - input-layer: compose() forwards each axis 1:1 (no mean-blend); add onReducedInput so the manifold tracks gamepad/MIDI position. - types: InputAction.phase, InputMode. Console (manifold/src/console/): - ConsoleApp: bind gamepad buttons to verdicts (RB up / LB down / X randomise / Y nudge / B undo / A-hold reposition); mirror composed position onto the manifold. - Drawers: rebuilt Inputs drawer (source picker, gamepad legend, MIDI device picker + batch-learn flow, learned-control meters). Engine (nisps/wasm, manifold/src/engine): - DefaultMLP widened MLP<2,..> -> MLP<32,..> (32 = MAX_AXES); each active axis gets a dedicated slot, unused slots held at 0 (inert). Rebuilt nisps.wasm (playground + manifold). - spine/engine-api: setInputs writes the full N-D vector (was dropping arr[2+]); primary pair keeps the 2-D pipeline; process() re-ticks the whole vector via spine.reprocess(). Tests: - parity_check/parity_wasm: ParityMLP -> 32 inputs, widen example bufs. - CMakeLists: build parity binary with -ffp-contract=off so native matches FMA-free WASM (training amplified the gap past 1e-5). Inputs dock is still an exclusive picker; mixing toggles, reshape modal, and the >2-D slider view (inputs-spec.md) are groundwork-laid but not yet wired. See docs/redesign/midi-gamepad-inputs-worklog.md.
2026-06-28 21:05:30 +02:00
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
Drag the on-screen manifold / XY pad. Two axes feed the net directly this is the default.
</p>
)}
{/* ---- Game Controller ---- */}
{inp.inputMode === 'gamepad' && depth === 'expanded' && (
<>
feat(manifold): MIDI + game controller inputs; widen ML net to N-D Wire the modular input layer into the Console and reshape the browser engine so input axes are genuine independent dimensions. Inputs (manifold/src/inputs/): - gamepad-source: emit press+release edges with standard-mapping labels (enables hold-and-move); single/double-stick already present. - midi-input-source: single-device selection + batch "MIDI Learn" (every CC swept while armed becomes an axis); notes stay discrete. - input-layer: compose() forwards each axis 1:1 (no mean-blend); add onReducedInput so the manifold tracks gamepad/MIDI position. - types: InputAction.phase, InputMode. Console (manifold/src/console/): - ConsoleApp: bind gamepad buttons to verdicts (RB up / LB down / X randomise / Y nudge / B undo / A-hold reposition); mirror composed position onto the manifold. - Drawers: rebuilt Inputs drawer (source picker, gamepad legend, MIDI device picker + batch-learn flow, learned-control meters). Engine (nisps/wasm, manifold/src/engine): - DefaultMLP widened MLP<2,..> -> MLP<32,..> (32 = MAX_AXES); each active axis gets a dedicated slot, unused slots held at 0 (inert). Rebuilt nisps.wasm (playground + manifold). - spine/engine-api: setInputs writes the full N-D vector (was dropping arr[2+]); primary pair keeps the 2-D pipeline; process() re-ticks the whole vector via spine.reprocess(). Tests: - parity_check/parity_wasm: ParityMLP -> 32 inputs, widen example bufs. - CMakeLists: build parity binary with -ffp-contract=off so native matches FMA-free WASM (training amplified the gap past 1e-5). Inputs dock is still an exclusive picker; mixing toggles, reshape modal, and the >2-D slider view (inputs-spec.md) are groundwork-laid but not yet wired. See docs/redesign/midi-gamepad-inputs-worklog.md.
2026-06-28 21:05:30 +02:00
<SectionLabel>Sticks</SectionLabel>
<Segmented
value={inp.gamepadStickMode}
onChange={inp.setGamepadStickMode}
options={[
{ value: 'single', label: 'One stick (2 ax)' },
{ value: 'double', label: 'Both sticks (4 ax)' },
]}
/>
<SectionLabel>Buttons</SectionLabel>
<div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
{GAMEPAD_LEGEND.map((g) => (
<div
key={g.btn}
style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 'var(--fs-xs)' }}
>
<Chip tone="var(--accent)">{g.btn}</Chip>
<span style={{ color: 'var(--fg-mute)' }}>{g.action}</span>
</div>
))}
</div>
<p style={{ fontSize: 9, color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
Connect a controller and press any button to wake it. Sticks drive the input map;
buttons fire the verdicts above.
</p>
</>
)}
feat(manifold): MIDI + game controller inputs; widen ML net to N-D Wire the modular input layer into the Console and reshape the browser engine so input axes are genuine independent dimensions. Inputs (manifold/src/inputs/): - gamepad-source: emit press+release edges with standard-mapping labels (enables hold-and-move); single/double-stick already present. - midi-input-source: single-device selection + batch "MIDI Learn" (every CC swept while armed becomes an axis); notes stay discrete. - input-layer: compose() forwards each axis 1:1 (no mean-blend); add onReducedInput so the manifold tracks gamepad/MIDI position. - types: InputAction.phase, InputMode. Console (manifold/src/console/): - ConsoleApp: bind gamepad buttons to verdicts (RB up / LB down / X randomise / Y nudge / B undo / A-hold reposition); mirror composed position onto the manifold. - Drawers: rebuilt Inputs drawer (source picker, gamepad legend, MIDI device picker + batch-learn flow, learned-control meters). Engine (nisps/wasm, manifold/src/engine): - DefaultMLP widened MLP<2,..> -> MLP<32,..> (32 = MAX_AXES); each active axis gets a dedicated slot, unused slots held at 0 (inert). Rebuilt nisps.wasm (playground + manifold). - spine/engine-api: setInputs writes the full N-D vector (was dropping arr[2+]); primary pair keeps the 2-D pipeline; process() re-ticks the whole vector via spine.reprocess(). Tests: - parity_check/parity_wasm: ParityMLP -> 32 inputs, widen example bufs. - CMakeLists: build parity binary with -ffp-contract=off so native matches FMA-free WASM (training amplified the gap past 1e-5). Inputs dock is still an exclusive picker; mixing toggles, reshape modal, and the >2-D slider view (inputs-spec.md) are groundwork-laid but not yet wired. See docs/redesign/midi-gamepad-inputs-worklog.md.
2026-06-28 21:05:30 +02:00
{/* ---- MIDI ---- */}
{inp.inputMode === 'midi' && depth === 'expanded' && (
feat(manifold): MIDI + game controller inputs; widen ML net to N-D Wire the modular input layer into the Console and reshape the browser engine so input axes are genuine independent dimensions. Inputs (manifold/src/inputs/): - gamepad-source: emit press+release edges with standard-mapping labels (enables hold-and-move); single/double-stick already present. - midi-input-source: single-device selection + batch "MIDI Learn" (every CC swept while armed becomes an axis); notes stay discrete. - input-layer: compose() forwards each axis 1:1 (no mean-blend); add onReducedInput so the manifold tracks gamepad/MIDI position. - types: InputAction.phase, InputMode. Console (manifold/src/console/): - ConsoleApp: bind gamepad buttons to verdicts (RB up / LB down / X randomise / Y nudge / B undo / A-hold reposition); mirror composed position onto the manifold. - Drawers: rebuilt Inputs drawer (source picker, gamepad legend, MIDI device picker + batch-learn flow, learned-control meters). Engine (nisps/wasm, manifold/src/engine): - DefaultMLP widened MLP<2,..> -> MLP<32,..> (32 = MAX_AXES); each active axis gets a dedicated slot, unused slots held at 0 (inert). Rebuilt nisps.wasm (playground + manifold). - spine/engine-api: setInputs writes the full N-D vector (was dropping arr[2+]); primary pair keeps the 2-D pipeline; process() re-ticks the whole vector via spine.reprocess(). Tests: - parity_check/parity_wasm: ParityMLP -> 32 inputs, widen example bufs. - CMakeLists: build parity binary with -ffp-contract=off so native matches FMA-free WASM (training amplified the gap past 1e-5). Inputs dock is still an exclusive picker; mixing toggles, reshape modal, and the >2-D slider view (inputs-spec.md) are groundwork-laid but not yet wired. See docs/redesign/midi-gamepad-inputs-worklog.md.
2026-06-28 21:05:30 +02:00
<>
<SectionLabel>Device</SectionLabel>
{inp.midiInputs.length === 0 ? (
<p style={{ fontSize: 9, color: 'var(--fg-dim)', margin: 0 }}>
No MIDI inputs detected. Connect a device it appears here automatically.
</p>
) : (
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
<Button
size="sm"
variant={inp.midiDeviceId === null ? 'primary' : 'secondary'}
onClick={() => inp.selectMidiDevice(null)}
>
All ports
</Button>
{inp.midiInputs.map((p) => (
<Button
feat(manifold): MIDI + game controller inputs; widen ML net to N-D Wire the modular input layer into the Console and reshape the browser engine so input axes are genuine independent dimensions. Inputs (manifold/src/inputs/): - gamepad-source: emit press+release edges with standard-mapping labels (enables hold-and-move); single/double-stick already present. - midi-input-source: single-device selection + batch "MIDI Learn" (every CC swept while armed becomes an axis); notes stay discrete. - input-layer: compose() forwards each axis 1:1 (no mean-blend); add onReducedInput so the manifold tracks gamepad/MIDI position. - types: InputAction.phase, InputMode. Console (manifold/src/console/): - ConsoleApp: bind gamepad buttons to verdicts (RB up / LB down / X randomise / Y nudge / B undo / A-hold reposition); mirror composed position onto the manifold. - Drawers: rebuilt Inputs drawer (source picker, gamepad legend, MIDI device picker + batch-learn flow, learned-control meters). Engine (nisps/wasm, manifold/src/engine): - DefaultMLP widened MLP<2,..> -> MLP<32,..> (32 = MAX_AXES); each active axis gets a dedicated slot, unused slots held at 0 (inert). Rebuilt nisps.wasm (playground + manifold). - spine/engine-api: setInputs writes the full N-D vector (was dropping arr[2+]); primary pair keeps the 2-D pipeline; process() re-ticks the whole vector via spine.reprocess(). Tests: - parity_check/parity_wasm: ParityMLP -> 32 inputs, widen example bufs. - CMakeLists: build parity binary with -ffp-contract=off so native matches FMA-free WASM (training amplified the gap past 1e-5). Inputs dock is still an exclusive picker; mixing toggles, reshape modal, and the >2-D slider view (inputs-spec.md) are groundwork-laid but not yet wired. See docs/redesign/midi-gamepad-inputs-worklog.md.
2026-06-28 21:05:30 +02:00
key={p.id}
size="sm"
feat(manifold): MIDI + game controller inputs; widen ML net to N-D Wire the modular input layer into the Console and reshape the browser engine so input axes are genuine independent dimensions. Inputs (manifold/src/inputs/): - gamepad-source: emit press+release edges with standard-mapping labels (enables hold-and-move); single/double-stick already present. - midi-input-source: single-device selection + batch "MIDI Learn" (every CC swept while armed becomes an axis); notes stay discrete. - input-layer: compose() forwards each axis 1:1 (no mean-blend); add onReducedInput so the manifold tracks gamepad/MIDI position. - types: InputAction.phase, InputMode. Console (manifold/src/console/): - ConsoleApp: bind gamepad buttons to verdicts (RB up / LB down / X randomise / Y nudge / B undo / A-hold reposition); mirror composed position onto the manifold. - Drawers: rebuilt Inputs drawer (source picker, gamepad legend, MIDI device picker + batch-learn flow, learned-control meters). Engine (nisps/wasm, manifold/src/engine): - DefaultMLP widened MLP<2,..> -> MLP<32,..> (32 = MAX_AXES); each active axis gets a dedicated slot, unused slots held at 0 (inert). Rebuilt nisps.wasm (playground + manifold). - spine/engine-api: setInputs writes the full N-D vector (was dropping arr[2+]); primary pair keeps the 2-D pipeline; process() re-ticks the whole vector via spine.reprocess(). Tests: - parity_check/parity_wasm: ParityMLP -> 32 inputs, widen example bufs. - CMakeLists: build parity binary with -ffp-contract=off so native matches FMA-free WASM (training amplified the gap past 1e-5). Inputs dock is still an exclusive picker; mixing toggles, reshape modal, and the >2-D slider view (inputs-spec.md) are groundwork-laid but not yet wired. See docs/redesign/midi-gamepad-inputs-worklog.md.
2026-06-28 21:05:30 +02:00
variant={inp.midiDeviceId === p.id ? 'primary' : 'secondary'}
onClick={() => inp.selectMidiDevice(p.id)}
>
feat(manifold): MIDI + game controller inputs; widen ML net to N-D Wire the modular input layer into the Console and reshape the browser engine so input axes are genuine independent dimensions. Inputs (manifold/src/inputs/): - gamepad-source: emit press+release edges with standard-mapping labels (enables hold-and-move); single/double-stick already present. - midi-input-source: single-device selection + batch "MIDI Learn" (every CC swept while armed becomes an axis); notes stay discrete. - input-layer: compose() forwards each axis 1:1 (no mean-blend); add onReducedInput so the manifold tracks gamepad/MIDI position. - types: InputAction.phase, InputMode. Console (manifold/src/console/): - ConsoleApp: bind gamepad buttons to verdicts (RB up / LB down / X randomise / Y nudge / B undo / A-hold reposition); mirror composed position onto the manifold. - Drawers: rebuilt Inputs drawer (source picker, gamepad legend, MIDI device picker + batch-learn flow, learned-control meters). Engine (nisps/wasm, manifold/src/engine): - DefaultMLP widened MLP<2,..> -> MLP<32,..> (32 = MAX_AXES); each active axis gets a dedicated slot, unused slots held at 0 (inert). Rebuilt nisps.wasm (playground + manifold). - spine/engine-api: setInputs writes the full N-D vector (was dropping arr[2+]); primary pair keeps the 2-D pipeline; process() re-ticks the whole vector via spine.reprocess(). Tests: - parity_check/parity_wasm: ParityMLP -> 32 inputs, widen example bufs. - CMakeLists: build parity binary with -ffp-contract=off so native matches FMA-free WASM (training amplified the gap past 1e-5). Inputs dock is still an exclusive picker; mixing toggles, reshape modal, and the >2-D slider view (inputs-spec.md) are groundwork-laid but not yet wired. See docs/redesign/midi-gamepad-inputs-worklog.md.
2026-06-28 21:05:30 +02:00
{p.name}
</Button>
feat(manifold): MIDI + game controller inputs; widen ML net to N-D Wire the modular input layer into the Console and reshape the browser engine so input axes are genuine independent dimensions. Inputs (manifold/src/inputs/): - gamepad-source: emit press+release edges with standard-mapping labels (enables hold-and-move); single/double-stick already present. - midi-input-source: single-device selection + batch "MIDI Learn" (every CC swept while armed becomes an axis); notes stay discrete. - input-layer: compose() forwards each axis 1:1 (no mean-blend); add onReducedInput so the manifold tracks gamepad/MIDI position. - types: InputAction.phase, InputMode. Console (manifold/src/console/): - ConsoleApp: bind gamepad buttons to verdicts (RB up / LB down / X randomise / Y nudge / B undo / A-hold reposition); mirror composed position onto the manifold. - Drawers: rebuilt Inputs drawer (source picker, gamepad legend, MIDI device picker + batch-learn flow, learned-control meters). Engine (nisps/wasm, manifold/src/engine): - DefaultMLP widened MLP<2,..> -> MLP<32,..> (32 = MAX_AXES); each active axis gets a dedicated slot, unused slots held at 0 (inert). Rebuilt nisps.wasm (playground + manifold). - spine/engine-api: setInputs writes the full N-D vector (was dropping arr[2+]); primary pair keeps the 2-D pipeline; process() re-ticks the whole vector via spine.reprocess(). Tests: - parity_check/parity_wasm: ParityMLP -> 32 inputs, widen example bufs. - CMakeLists: build parity binary with -ffp-contract=off so native matches FMA-free WASM (training amplified the gap past 1e-5). Inputs dock is still an exclusive picker; mixing toggles, reshape modal, and the >2-D slider view (inputs-spec.md) are groundwork-laid but not yet wired. See docs/redesign/midi-gamepad-inputs-worklog.md.
2026-06-28 21:05:30 +02:00
))}
</div>
)}
feat(manifold): MIDI + game controller inputs; widen ML net to N-D Wire the modular input layer into the Console and reshape the browser engine so input axes are genuine independent dimensions. Inputs (manifold/src/inputs/): - gamepad-source: emit press+release edges with standard-mapping labels (enables hold-and-move); single/double-stick already present. - midi-input-source: single-device selection + batch "MIDI Learn" (every CC swept while armed becomes an axis); notes stay discrete. - input-layer: compose() forwards each axis 1:1 (no mean-blend); add onReducedInput so the manifold tracks gamepad/MIDI position. - types: InputAction.phase, InputMode. Console (manifold/src/console/): - ConsoleApp: bind gamepad buttons to verdicts (RB up / LB down / X randomise / Y nudge / B undo / A-hold reposition); mirror composed position onto the manifold. - Drawers: rebuilt Inputs drawer (source picker, gamepad legend, MIDI device picker + batch-learn flow, learned-control meters). Engine (nisps/wasm, manifold/src/engine): - DefaultMLP widened MLP<2,..> -> MLP<32,..> (32 = MAX_AXES); each active axis gets a dedicated slot, unused slots held at 0 (inert). Rebuilt nisps.wasm (playground + manifold). - spine/engine-api: setInputs writes the full N-D vector (was dropping arr[2+]); primary pair keeps the 2-D pipeline; process() re-ticks the whole vector via spine.reprocess(). Tests: - parity_check/parity_wasm: ParityMLP -> 32 inputs, widen example bufs. - CMakeLists: build parity binary with -ffp-contract=off so native matches FMA-free WASM (training amplified the gap past 1e-5). Inputs dock is still an exclusive picker; mixing toggles, reshape modal, and the >2-D slider view (inputs-spec.md) are groundwork-laid but not yet wired. See docs/redesign/midi-gamepad-inputs-worklog.md.
2026-06-28 21:05:30 +02:00
<SectionLabel>MIDI Learn</SectionLabel>
{inp.midiLearnArmed ? (
<div
style={{
padding: '8px 10px',
background: 'var(--bg-2)',
border: '1px solid var(--accent-2)',
borderRadius: 'var(--r-1)',
display: 'flex',
flexDirection: 'column',
gap: 8,
}}
>
<span style={{ fontSize: 'var(--fs-xs)', color: 'var(--accent-2)', lineHeight: 1.5 }}>
Move all of the controls you want to use, then click Done. Each knob or fader you
touch becomes an input.
</span>
<Button size="sm" variant="primary" onClick={() => inp.armMidiLearn(false)}>
Done ({inp.midiBindings.length} learned)
</Button>
</div>
) : (
feat(manifold): MIDI + game controller inputs; widen ML net to N-D Wire the modular input layer into the Console and reshape the browser engine so input axes are genuine independent dimensions. Inputs (manifold/src/inputs/): - gamepad-source: emit press+release edges with standard-mapping labels (enables hold-and-move); single/double-stick already present. - midi-input-source: single-device selection + batch "MIDI Learn" (every CC swept while armed becomes an axis); notes stay discrete. - input-layer: compose() forwards each axis 1:1 (no mean-blend); add onReducedInput so the manifold tracks gamepad/MIDI position. - types: InputAction.phase, InputMode. Console (manifold/src/console/): - ConsoleApp: bind gamepad buttons to verdicts (RB up / LB down / X randomise / Y nudge / B undo / A-hold reposition); mirror composed position onto the manifold. - Drawers: rebuilt Inputs drawer (source picker, gamepad legend, MIDI device picker + batch-learn flow, learned-control meters). Engine (nisps/wasm, manifold/src/engine): - DefaultMLP widened MLP<2,..> -> MLP<32,..> (32 = MAX_AXES); each active axis gets a dedicated slot, unused slots held at 0 (inert). Rebuilt nisps.wasm (playground + manifold). - spine/engine-api: setInputs writes the full N-D vector (was dropping arr[2+]); primary pair keeps the 2-D pipeline; process() re-ticks the whole vector via spine.reprocess(). Tests: - parity_check/parity_wasm: ParityMLP -> 32 inputs, widen example bufs. - CMakeLists: build parity binary with -ffp-contract=off so native matches FMA-free WASM (training amplified the gap past 1e-5). Inputs dock is still an exclusive picker; mixing toggles, reshape modal, and the >2-D slider view (inputs-spec.md) are groundwork-laid but not yet wired. See docs/redesign/midi-gamepad-inputs-worklog.md.
2026-06-28 21:05:30 +02:00
<div style={{ display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap' }}>
<Button size="sm" variant="secondary" onClick={() => inp.armMidiLearn(true)}>
{inp.midiBindings.length ? 'Learn more…' : 'Start MIDI Learn'}
</Button>
{inp.midiBindings.length > 0 && (
<Button size="sm" variant="secondary" onClick={inp.clearMidiBindings}>
Clear all
</Button>
)}
</div>
)}
feat(manifold): MIDI + game controller inputs; widen ML net to N-D Wire the modular input layer into the Console and reshape the browser engine so input axes are genuine independent dimensions. Inputs (manifold/src/inputs/): - gamepad-source: emit press+release edges with standard-mapping labels (enables hold-and-move); single/double-stick already present. - midi-input-source: single-device selection + batch "MIDI Learn" (every CC swept while armed becomes an axis); notes stay discrete. - input-layer: compose() forwards each axis 1:1 (no mean-blend); add onReducedInput so the manifold tracks gamepad/MIDI position. - types: InputAction.phase, InputMode. Console (manifold/src/console/): - ConsoleApp: bind gamepad buttons to verdicts (RB up / LB down / X randomise / Y nudge / B undo / A-hold reposition); mirror composed position onto the manifold. - Drawers: rebuilt Inputs drawer (source picker, gamepad legend, MIDI device picker + batch-learn flow, learned-control meters). Engine (nisps/wasm, manifold/src/engine): - DefaultMLP widened MLP<2,..> -> MLP<32,..> (32 = MAX_AXES); each active axis gets a dedicated slot, unused slots held at 0 (inert). Rebuilt nisps.wasm (playground + manifold). - spine/engine-api: setInputs writes the full N-D vector (was dropping arr[2+]); primary pair keeps the 2-D pipeline; process() re-ticks the whole vector via spine.reprocess(). Tests: - parity_check/parity_wasm: ParityMLP -> 32 inputs, widen example bufs. - CMakeLists: build parity binary with -ffp-contract=off so native matches FMA-free WASM (training amplified the gap past 1e-5). Inputs dock is still an exclusive picker; mixing toggles, reshape modal, and the >2-D slider view (inputs-spec.md) are groundwork-laid but not yet wired. See docs/redesign/midi-gamepad-inputs-worklog.md.
2026-06-28 21:05:30 +02:00
{inp.midiBindings.length > 0 && (
<>
<SectionLabel>Learned controls</SectionLabel>
<div style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
{inp.midiBindings.map((b, i) => (
<MidiInputMeter
key={`${b.kind}-${b.number}-${b.channel}`}
label={b.label}
value={b.value}
onClear={() => inp.clearMidiBinding(i)}
/>
))}
</div>
</>
)}
</>
)}
feat(manifold): MIDI + game controller inputs; widen ML net to N-D Wire the modular input layer into the Console and reshape the browser engine so input axes are genuine independent dimensions. Inputs (manifold/src/inputs/): - gamepad-source: emit press+release edges with standard-mapping labels (enables hold-and-move); single/double-stick already present. - midi-input-source: single-device selection + batch "MIDI Learn" (every CC swept while armed becomes an axis); notes stay discrete. - input-layer: compose() forwards each axis 1:1 (no mean-blend); add onReducedInput so the manifold tracks gamepad/MIDI position. - types: InputAction.phase, InputMode. Console (manifold/src/console/): - ConsoleApp: bind gamepad buttons to verdicts (RB up / LB down / X randomise / Y nudge / B undo / A-hold reposition); mirror composed position onto the manifold. - Drawers: rebuilt Inputs drawer (source picker, gamepad legend, MIDI device picker + batch-learn flow, learned-control meters). Engine (nisps/wasm, manifold/src/engine): - DefaultMLP widened MLP<2,..> -> MLP<32,..> (32 = MAX_AXES); each active axis gets a dedicated slot, unused slots held at 0 (inert). Rebuilt nisps.wasm (playground + manifold). - spine/engine-api: setInputs writes the full N-D vector (was dropping arr[2+]); primary pair keeps the 2-D pipeline; process() re-ticks the whole vector via spine.reprocess(). Tests: - parity_check/parity_wasm: ParityMLP -> 32 inputs, widen example bufs. - CMakeLists: build parity binary with -ffp-contract=off so native matches FMA-free WASM (training amplified the gap past 1e-5). Inputs dock is still an exclusive picker; mixing toggles, reshape modal, and the >2-D slider view (inputs-spec.md) are groundwork-laid but not yet wired. See docs/redesign/midi-gamepad-inputs-worklog.md.
2026-06-28 21:05:30 +02:00
{/* ---- Dedicated-dimensions note (only with >2 active axes) ---- */}
{inp.axisCount > 2 && depth === 'expanded' && (
feat(manifold): MIDI + game controller inputs; widen ML net to N-D Wire the modular input layer into the Console and reshape the browser engine so input axes are genuine independent dimensions. Inputs (manifold/src/inputs/): - gamepad-source: emit press+release edges with standard-mapping labels (enables hold-and-move); single/double-stick already present. - midi-input-source: single-device selection + batch "MIDI Learn" (every CC swept while armed becomes an axis); notes stay discrete. - input-layer: compose() forwards each axis 1:1 (no mean-blend); add onReducedInput so the manifold tracks gamepad/MIDI position. - types: InputAction.phase, InputMode. Console (manifold/src/console/): - ConsoleApp: bind gamepad buttons to verdicts (RB up / LB down / X randomise / Y nudge / B undo / A-hold reposition); mirror composed position onto the manifold. - Drawers: rebuilt Inputs drawer (source picker, gamepad legend, MIDI device picker + batch-learn flow, learned-control meters). Engine (nisps/wasm, manifold/src/engine): - DefaultMLP widened MLP<2,..> -> MLP<32,..> (32 = MAX_AXES); each active axis gets a dedicated slot, unused slots held at 0 (inert). Rebuilt nisps.wasm (playground + manifold). - spine/engine-api: setInputs writes the full N-D vector (was dropping arr[2+]); primary pair keeps the 2-D pipeline; process() re-ticks the whole vector via spine.reprocess(). Tests: - parity_check/parity_wasm: ParityMLP -> 32 inputs, widen example bufs. - CMakeLists: build parity binary with -ffp-contract=off so native matches FMA-free WASM (training amplified the gap past 1e-5). Inputs dock is still an exclusive picker; mixing toggles, reshape modal, and the >2-D slider view (inputs-spec.md) are groundwork-laid but not yet wired. See docs/redesign/midi-gamepad-inputs-worklog.md.
2026-06-28 21:05:30 +02:00
<p style={{ fontSize: 9, color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
The net has {inp.engineInputSize} dedicated inputs each active axis drives its own
dimension 1:1 (no blending).
{inputMismatch
? ` Its ${inp.engineInputSize} slots don't match the ${inp.axisCount} active axes; ` +
`changing the layout offers a reshape to ${inp.axisCount} inputs (warm-started).`
: ''}
feat(manifold): MIDI + game controller inputs; widen ML net to N-D Wire the modular input layer into the Console and reshape the browser engine so input axes are genuine independent dimensions. Inputs (manifold/src/inputs/): - gamepad-source: emit press+release edges with standard-mapping labels (enables hold-and-move); single/double-stick already present. - midi-input-source: single-device selection + batch "MIDI Learn" (every CC swept while armed becomes an axis); notes stay discrete. - input-layer: compose() forwards each axis 1:1 (no mean-blend); add onReducedInput so the manifold tracks gamepad/MIDI position. - types: InputAction.phase, InputMode. Console (manifold/src/console/): - ConsoleApp: bind gamepad buttons to verdicts (RB up / LB down / X randomise / Y nudge / B undo / A-hold reposition); mirror composed position onto the manifold. - Drawers: rebuilt Inputs drawer (source picker, gamepad legend, MIDI device picker + batch-learn flow, learned-control meters). Engine (nisps/wasm, manifold/src/engine): - DefaultMLP widened MLP<2,..> -> MLP<32,..> (32 = MAX_AXES); each active axis gets a dedicated slot, unused slots held at 0 (inert). Rebuilt nisps.wasm (playground + manifold). - spine/engine-api: setInputs writes the full N-D vector (was dropping arr[2+]); primary pair keeps the 2-D pipeline; process() re-ticks the whole vector via spine.reprocess(). Tests: - parity_check/parity_wasm: ParityMLP -> 32 inputs, widen example bufs. - CMakeLists: build parity binary with -ffp-contract=off so native matches FMA-free WASM (training amplified the gap past 1e-5). Inputs dock is still an exclusive picker; mixing toggles, reshape modal, and the >2-D slider view (inputs-spec.md) are groundwork-laid but not yet wired. See docs/redesign/midi-gamepad-inputs-worklog.md.
2026-06-28 21:05:30 +02:00
</p>
)}
</>
);
}
// ===========================================================================
// 3. OUTPUTS / ROUTING (dock-spec §3, §4)
// ===========================================================================
const VISUAL_NAMES = [
'Flow', 'Scale', 'Speed', 'Hue', 'Spread', 'Size', 'Trail', 'Turb', 'Attract', 'Radius',
'DispRate', 'DispAmt', 'Lifetime', 'Respawn', 'Advection', 'Inertia', 'Drag', 'Repulse',
'RepCnt', 'RepRate',
];
/**
* Per-Mode config section shown ABOVE the per-output rows. The synth Mode shows
* transport + tempo; the particle Mode names the outputs; MIDI/OSC/Editor show
* their own affordances. Replaces the removed Synth + Visual drawers.
*/
function ModeConfig(ctx: ConsoleCtx, depth: DrawerDepth) {
switch (ctx.outputMode) {
case 'synth':
return (
<>
<SectionLabel>Transport</SectionLabel>
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
<Button size="sm" variant={ctx.audioStarted ? 'secondary' : 'primary'} onClick={ctx.onToggleAudio}>
{ctx.audioStarted ? 'pause' : 'play'}
</Button>
<span style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)' }}>audio starts on the play gesture</span>
</div>
{depth === 'expanded' && (
refactor(manifold): delete the dead console UI stratum Phase 1 group 5 (S15, S16, S18, S19, L22, L23, L20, L21, L1 delete-half). - S15: the four-way focus/altitude system. setFocus was never called anywhere, so only the 'composite' branch was reachable. Deleted SplitStage, ReadoutStrip, InputMini, AltitudeNav, CompactAxis, the UI Focus type/prop, the focus branches, stripPinned and the vacuous keyboard gates. MiniMeters kept; engine.feedback.setFocus (a different, live thing) untouched. - S16 + L1: the decorative stratum that rendered real-looking controls driving nothing — A/B machinery, the fake seed, seededGradient + weightsRevision, snapshots, master volume, bpm, and the learningRate/decay/tame/spreadLevel sliders with their Drawers rows. Each was confirmed self-referential first. NOTE a real behaviour change falls out of dropping `snapshots`: Undo outside an active explore-and-place session used to pop a UI-only snapshot that restored noiseCap/seed. It is now simply inactive unless a genuine core-backed scratchpad undo exists. Geometric-dislike mode never had a real undo primitive in the core, so only the fake path is gone. - S18: BackendAdvanced.tsx and its Drawers block. It self-described as a duplicate of the inline OutputsBackendConfig editor and BOTH rendered in the same expanded drawer. OutputsBackendConfig already covers every backend. - S19: pruned ConsoleCtx to the fields Dock/Drawers/OutputsBackendConfig actually read; deleted the Axes type + axes/setAxis and the preset/setPreset/offsetActive chain (permanently 'Sculpt'/false). KEPT ctx.modes and ctx.setModeId despite having no reader today — the Phase 5 instrument picker (§7.6, adopted) is built on exactly that plumbing. - L22: the 5 dead primitives (Panel, StatusLine, ControlAxis, CurvePlot, Sparkline) and their barrel exports, plus the now-dead .mf-axis-input CSS. - L23: OutputControl/toOutputControl, ModeIconComponent and the BACKENDS catalogue; Drawers now reads modeDesc.label/description from OUTPUT_MODES, the surviving single catalogue. - L20: the solo-mode selector's two unimplemented options no longer pretend to be selectable. - L21: FeedbackController vestiges — seed/undoDepth options, maxUndo, and six ControllerEngine members nothing called (the finding named three; the other three are used on the real EngineApi by debug/probe.ts, a different interface, so removing them from ControllerEngine is safe). Gates: run-all-tests.sh ALL GREEN (typecheck, 33 Playwright specs).
2026-07-21 12:49:25 +02:00
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
The active engine follows the selected mode ({ctx.mode.label}).
{/* TODO(dock-spec §5): arpeggiator + tiered synth presets + the
18-section group-override matrix are workstream E. Master
volume + bpm sliders were deleted (simplification audit
S16) they drove no engine parameter. */}
</p>
)}
</>
);
case 'editor':
return (
<>
<SectionLabel>MEMLNaut · USB serial</SectionLabel>
<EditorPanel />
</>
);
case 'particles':
return depth === 'expanded' ? (
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
Flow-field visualiser driven by the first {Math.min(20, ctx.params.length)} outputs (no audio).
{/* TODO(backends-spec §4): port FlowFieldVisualizer + visual preset chips. */}
</p>
) : null;
case 'midi':
// The full MIDI config (port picker, CC count, per-output CC/channel/name)
// + preset bar render via OutputsBackendConfig in RoutingDrawer below.
return depth === 'expanded' ? (
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
Each output sends a real Web MIDI CC. Pick a port and set CC# / channel per output.
</p>
) : null;
case 'osc':
return depth === 'expanded' ? (
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
Each output sends to an OSC path with a physical range, over the WebSocket bridge.
</p>
) : null;
default:
return null;
}
}
function RoutingDrawer(ctx: ConsoleCtx, depth: DrawerDepth) {
const activeParams = ctx.params.slice(0, ctx.displayOutputCount);
const values = shapeValues(activeParams, null); // bar uses shaped held/live value snapshot
const counts = activeParams.reduce<Record<string, number>>((a, p) => {
a[p.status] = (a[p.status] || 0) + 1;
return a;
}, {});
const mutedN = activeParams.filter((p) => p.muted).length;
const modeDesc = outputModeDescriptor(ctx.outputMode);
// The particle Mode names its outputs; otherwise use the param names.
const nameFor = (idx: number, fallback: string) =>
ctx.outputMode === 'particles' ? VISUAL_NAMES[idx] ?? fallback : fallback;
const expanded = depth === 'expanded';
const rows = expanded ? activeParams : activeParams.slice(0, 6);
return (
<>
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', alignItems: 'center' }}>
<Chip tone="var(--accent)">{modeDesc.label}</Chip>
<Chip tone="var(--accent)">live {counts.live || 0}</Chip>
<Chip tone="var(--accent-2)">fixed {counts.fixed || 0}</Chip>
<Chip>off {counts.off || 0}</Chip>
<Chip tone="var(--danger)">muted {mutedN}</Chip>
<BackendStatusChip ctx={ctx} />
</div>
{ModeConfig(ctx, depth)}
{/* Specialised per-backend config + named-preset bar (MIDI/OSC); hidden when condensed. */}
{expanded && <OutputsBackendConfig ctx={ctx} backend={modeDesc.backend} />}
<SectionLabel>Outputs · M mute · S arm · off/fixed/live</SectionLabel>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 4,
maxHeight: expanded ? 460 : 220,
overflow: 'auto',
}}
>
{rows.map((p) => {
const i = ctx.params.indexOf(p);
const labelled = { ...p, name: nameFor(i, p.name) };
return (
<OutputControlRow
key={i}
param={labelled}
value={values[i] ?? 0}
onChange={(patch) => ctx.setParam(i, patch)}
showCurve={expanded}
/>
);
})}
</div>
{!expanded && activeParams.length > 6 && (
<span style={{ fontSize: 9, color: 'var(--fg-dim)' }}>+{activeParams.length - 6} more expand to edit</span>
)}
</>
);
}
// ===========================================================================
// 4. SETTINGS (operator dock restructure — settings-store)
// ===========================================================================
const ICON_COLOUR_OPTS: { value: UnfocusedIconColour; label: string }[] = [
{ value: 'off-white', label: 'Off-white' },
{ value: 'white', label: 'White' },
{ value: 'orange', label: 'Orange' },
];
const INPUT_MAP_OPTS: { value: InputMapMode; label: string }[] = [
{ value: 'follow-mode', label: 'Follow mode' },
{ value: 'rectangular', label: 'Rectangular' },
{ value: 'circular', label: 'Circular' },
];
function SettingsDrawer({ depth }: { ctx: ConsoleCtx; depth: DrawerDepth }) {
const { settings, set } = useSettings();
return (
<>
<SectionLabel>Icons</SectionLabel>
<Switch
checked={settings.monochromeIcons}
onChange={(v) => set('monochromeIcons', v)}
label="Monochrome icons"
/>
{depth === 'expanded' && (
<>
<div style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)' }}>Unfocused icon colour</div>
<PillToggle
value={settings.unfocusedIconColour}
onChange={(v) => set('unfocusedIconColour', v as UnfocusedIconColour)}
options={ICON_COLOUR_OPTS}
/>
<p style={{ fontSize: 9, color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
Focused / active icons are always accent orange. This sets the resting colour of unfocused
icons (preview below).
</p>
<div style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
<span style={{ color: unfocusedIconCss(settings.unfocusedIconColour), display: 'inline-flex' }}>
<SettingsIcon size={20} />
</span>
<span style={{ color: 'var(--accent)', display: 'inline-flex' }}>
<SettingsIcon size={20} />
</span>
</div>
</>
)}
<SectionLabel>Input map</SectionLabel>
<PillToggle
value={settings.inputMap}
onChange={(v) => set('inputMap', v as InputMapMode)}
options={INPUT_MAP_OPTS}
/>
{depth === 'expanded' && (
<p style={{ fontSize: 9, color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
The 2D input surface: a rectangular XY map or a circular joystick-style disc. "Follow mode"
uses the active mode's declared input (joystick circular, else rectangular).
</p>
)}
<SectionLabel>Chrome</SectionLabel>
<Slider
label="Corner radius"
value={settings.cornerRadius}
min={0}
max={14}
step={1}
unit="px"
onChange={(v) => set('cornerRadius', Math.round(v))}
/>
{depth === 'expanded' && (
<p style={{ fontSize: 9, color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
Roundness of buttons, control rows, dock icons and panels. Pills and the circular verdict
buttons are intentionally exempt. Default 2px.
</p>
)}
</>
);
}
// ===========================================================================
// 5. HELP
// ===========================================================================
const KEYS: [string, string][] = [
['1', 'down / explore'],
['2', 'commit +'],
['space / ↑', 'commit +'],
['↓', 'down / explore'],
['35', 'open drawers'],
['\\', 'expand drawer'],
['z', 'undo'],
['[ ] =', 'split (composite)'],
['dbl-click mark', 'follow mouse (Esc exits)'],
];
function HelpDrawer() {
return (
<>
<SectionLabel>Keyboard</SectionLabel>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
{KEYS.map(([k, v]) => (
<div key={k} style={{ display: 'flex', justifyContent: 'space-between', fontSize: 'var(--fs-xs)' }}>
<kbd
style={{
background: 'var(--bg-2)',
border: '1px solid var(--line)',
borderRadius: 'var(--r-1)',
padding: '1px 6px',
color: 'var(--accent)',
}}
>
{k}
</kbd>
<span style={{ color: 'var(--fg-mute)' }}>{v}</span>
</div>
))}
</div>
<SectionLabel>The loop</SectionLabel>
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: 0, lineHeight: 1.7 }}>
Drag the manifold to explore. Hear something good + to keep it. Wrong to push away or
re-roll (set the behaviour in the Learning drawer). Went too far undo. The dock reveals
exactly as much machinery as you reach for.
</p>
<SectionLabel>Learn more</SectionLabel>
<a
href="animations/"
target="_blank"
rel="noopener"
style={{
display: 'inline-flex',
alignItems: 'center',
gap: 6,
fontSize: 'var(--fs-xs)',
color: 'var(--accent-2)',
border: '1px solid var(--line)',
borderRadius: 'var(--r-1)',
padding: '5px 10px',
width: 'fit-content',
textDecoration: 'none',
}}
>
Watch the explainers + interactive demos
</a>
</>
);
}
export interface DrawerSection {
/** Monochrome inline-SVG icon (currentColor-driven by the dock button). */
icon: ReactNode;
/** Prior colour-emoji glyph, used when monochrome icons are OFF. */
glyph: string;
label: string;
render: (ctx: ConsoleCtx, depth: DrawerDepth) => ReactNode;
}
export const DRAWERS: Record<DrawerKey, DrawerSection> = {
learn: { icon: <LearningIcon />, glyph: '🧠', label: 'Learning', render: LearningDrawer },
inputs: { icon: <InputsIcon />, glyph: '🎚', label: 'Inputs', render: InputsDrawer },
route: { icon: <OutputsIcon />, glyph: '🔀', label: 'Outputs', render: RoutingDrawer },
settings: { icon: <SettingsIcon />, glyph: '⚙', label: 'Settings', render: (c, d) => <SettingsDrawer ctx={c} depth={d} /> },
help: { icon: <HelpIcon />, glyph: '?', label: 'Help', render: HelpDrawer },
};