From f75f683f40079a804872e659a9831be706343831 Mon Sep 17 00:00:00 2001 From: monkey-w1n5t0n Date: Sat, 25 Jul 2026 13:55:19 +0200 Subject: [PATCH] fix(manifold): sync output sliders with backend count --- manifold/ONBOARDING.md | 6 ++- manifold/src/console/ConsoleApp.tsx | 49 +++++++++++++------ manifold/src/console/Drawers.tsx | 13 ++--- manifold/src/console/OutputStage.tsx | 2 + manifold/src/console/output-mode.ts | 19 +++++++ manifold/src/console/types.ts | 2 + .../tests/e2e/output-display-count.spec.ts | 28 +++++++++++ manifold/tests/output-display-count.test.ts | 18 +++++++ 8 files changed, 113 insertions(+), 24 deletions(-) create mode 100644 manifold/tests/e2e/output-display-count.spec.ts create mode 100644 manifold/tests/output-display-count.test.ts diff --git a/manifold/ONBOARDING.md b/manifold/ONBOARDING.md index c843d4d..ac4696d 100644 --- a/manifold/ONBOARDING.md +++ b/manifold/ONBOARDING.md @@ -119,8 +119,10 @@ double-click exits). `OutputStage.tsx` is the output columns; drag a bar to set `compact` prop for the narrow pane. - **Output modes** (the TOP dock selector, NOT the same axis as `focus`): `src/console/output-mode.ts` - defines `OUTPUT_MODES` = **particles** (default) / midi / osc / synth / editor, each mapping to a - `BackendId`. `DEFAULT_OUTPUT_MODE='particles'`. + defines `OUTPUT_MODES` = **particles** (default) / midi / osc / cv / synth / editor, each mapping to a + `BackendId`. `DEFAULT_OUTPUT_MODE='particles'`. `outputDisplayCount()` is the shared presentation + boundary for the stage and routing rows: MIDI uses its configured CC count, while backends without + a separate count present the full mode parameter set. This does not reshape the MLP or clear examples. - `src/console/output-mode.ts`, `types.ts`, `model.ts` are the shared vocabulary — read these first when touching anything cross-cutting: - `types.ts`: `Focus`, `OutputMode`, `DrawerKey`, `DrawerDepth`, `FeedbackModeUI`, `SoloMode`, diff --git a/manifold/src/console/ConsoleApp.tsx b/manifold/src/console/ConsoleApp.tsx index 63e4301..37bd08c 100644 --- a/manifold/src/console/ConsoleApp.tsx +++ b/manifold/src/console/ConsoleApp.tsx @@ -53,22 +53,25 @@ import type { import type { BackendId } from '../dock/output-state'; import { buildArmMask } from '../dock/output-state'; import { FeedbackController, type ProtoFeedbackMode } from '../feedback'; -import { DEFAULT_OUTPUT_MODE, outputModeDescriptor } from './output-mode'; +import { DEFAULT_OUTPUT_MODE, outputDisplayCount, outputModeDescriptor } from './output-mode'; import { useSettings, resolveInputMap } from '../settings/settings-store'; import { useBackendManager } from '../backends'; import { useInputLayer } from '../inputs'; /** - * Instrument-mode debug seam, installed on `window.__mf` under `?debug=1` - * (see the effect in ConsoleApp). UI-level analogue of the engine - * `window.__nisps` probe — lets Playwright drive mode switches and read the - * rendered param count, since no in-UI mode picker exists yet. + * Console debug seam, installed on `window.__mf` under `?debug=1` (see the + * effect in ConsoleApp). UI-level analogue of the engine `window.__nisps` + * probe — lets Playwright drive instrument/output configuration and inspect + * the resulting presentation. */ export interface MfDebugHook { setMode: (id: string) => void; getModeId: () => string; paramCount: () => number; modeIds: () => string[]; + setOutputMode: (mode: OutputMode) => void; + setMidiCcCount: (count: number) => void; + displayOutputCount: () => number; } declare global { @@ -139,6 +142,9 @@ export function ConsoleApp() { // named-preset system; these are the live working values. const [midiOutputId, setMidiOutputId] = useState(null); const [midiCcCount, setMidiCcCount] = useState(8); + const displayOutputCount = outputDisplayCount(outputMode, params.length, { + midi: midiCcCount, + }); const [oscUrl, setOscUrl] = useState('ws://localhost:8765'); const [oscSendRaw, setOscSendRaw] = useState(false); // VCV bridge: WS URL of the Deno bridge that relays to the VCV module over UDP @@ -322,12 +328,11 @@ export function ConsoleApp() { if (engine && n != null) engine.reshape({ inputSize: n }); }; - // ---- Debug seam for instrument-mode switching (`?debug=1`) ------------------ + // ---- Console debug seam (`?debug=1`) ---------------------------------------- // There is no instrument-mode picker in the UI yet (ctx.modes/setModeId are - // plumbed but unrendered), so Playwright drives mode switches through this - // window hook — the UI-level analogue of the engine `window.__nisps` probe. - // Exposes the current modeId, the rendered param count, and the mode ids so - // the schema-modes e2e can switch a mode and assert the derived shape. + // plumbed but unrendered), so Playwright drives instrument/output changes + // through this window hook — the UI-level analogue of the engine + // `window.__nisps` probe. useEffect(() => { if (typeof window === 'undefined') return; const q = new URLSearchParams(window.location.search); @@ -337,11 +342,14 @@ export function ConsoleApp() { getModeId: () => modeId, paramCount: () => params.length, modeIds: () => MF_MODES.map((m) => m.id), + setOutputMode, + setMidiCcCount, + displayOutputCount: () => displayOutputCount, }; return () => { if (window.__mf) delete window.__mf; }; - }, [modeId, params]); + }, [modeId, params, displayOutputCount]); // 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 @@ -408,6 +416,14 @@ export function ConsoleApp() { // eslint-disable-next-line react-hooks/exhaustive-deps [params, version, engine], ); + const displayedParams = useMemo( + () => params.slice(0, displayOutputCount), + [params, displayOutputCount], + ); + const displayedValues = useMemo( + () => values.slice(0, displayOutputCount), + [values, displayOutputCount], + ); /** Plot a feedback marker at the input location it was given (session-scoped). */ const pushMarker = (at: [number, number], polarity: 'positive' | 'negative') => @@ -752,6 +768,7 @@ export function ConsoleApp() { }, params, setParam, + displayOutputCount, outputMode, setOutputMode, // ---- output backend transport ---- @@ -863,8 +880,8 @@ export function ConsoleApp() { engine={engine} version={version} pos={pos} - layerCount={Math.min(params.length, 8)} - names={params.map((p) => p.name)} + layerCount={Math.min(displayedParams.length, 8)} + names={displayedParams.map((p) => p.name)} /> )} @@ -876,7 +893,7 @@ export function ConsoleApp() { borderLeft: '1px solid var(--line)', }} > - + ) : outputMode === 'particles' ? ( @@ -894,8 +911,8 @@ export function ConsoleApp() { variant={inputMapVariant} follow={follow} onLongPress={addPin} - params={params} - values={values} + params={displayedParams} + values={displayedValues} onChange={setParam} /> )} diff --git a/manifold/src/console/Drawers.tsx b/manifold/src/console/Drawers.tsx index 25f1c4d..b8fed14 100644 --- a/manifold/src/console/Drawers.tsx +++ b/manifold/src/console/Drawers.tsx @@ -606,19 +606,20 @@ function ModeConfig(ctx: ConsoleCtx, depth: DrawerDepth) { } function RoutingDrawer(ctx: ConsoleCtx, depth: DrawerDepth) { - const values = shapeValues(ctx.params, null); // bar uses shaped held/live value snapshot - const counts = ctx.params.reduce>((a, p) => { + const activeParams = ctx.params.slice(0, ctx.displayOutputCount); + const values = shapeValues(activeParams, null); // bar uses shaped held/live value snapshot + const counts = activeParams.reduce>((a, p) => { a[p.status] = (a[p.status] || 0) + 1; return a; }, {}); - const mutedN = ctx.params.filter((p) => p.muted).length; + 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 ? ctx.params : ctx.params.slice(0, 6); + const rows = expanded ? activeParams : activeParams.slice(0, 6); return ( <>
@@ -656,8 +657,8 @@ function RoutingDrawer(ctx: ConsoleCtx, depth: DrawerDepth) { ); })}
- {!expanded && ctx.params.length > 6 && ( - +{ctx.params.length - 6} more — expand to edit + {!expanded && activeParams.length > 6 && ( + +{activeParams.length - 6} more — expand to edit )} ); diff --git a/manifold/src/console/OutputStage.tsx b/manifold/src/console/OutputStage.tsx index 734ba72..46d7aea 100644 --- a/manifold/src/console/OutputStage.tsx +++ b/manifold/src/console/OutputStage.tsx @@ -76,6 +76,8 @@ export function OutputStage({ params, values, onChange, compact = false }: Outpu return (
m.id === id) ?? OUTPUT_MODES[0]; } + +/** + * Number of output controls the active backend presents. + * + * The model may expose more parameters than a backend currently maps (MIDI is + * the live example: its CC count is adjustable). Keep that presentation + * boundary separate from the model arity so changing a backend count does not + * silently reshape the net and clear its examples. + */ +export function outputDisplayCount( + id: OutputMode, + availableCount: number, + configuredCounts: Partial> = {}, +): number { + const available = Math.max(0, Math.floor(availableCount)); + const configured = configuredCounts[id]; + if (configured === undefined || !Number.isFinite(configured)) return available; + return Math.max(0, Math.min(available, Math.floor(configured))); +} diff --git a/manifold/src/console/types.ts b/manifold/src/console/types.ts index fe19b66..2edddee 100644 --- a/manifold/src/console/types.ts +++ b/manifold/src/console/types.ts @@ -76,6 +76,8 @@ export interface ConsoleCtx { params: MFParam[]; /** Patch one output row in the shared store (drives stage + dock in sync). */ setParam: (i: number, patch: Partial) => void; + /** Active backend outputs currently presented by the stage + routing rows. */ + displayOutputCount: number; // ---- Output backend transport (backends-spec §1–§5) ---- /** Live status of the active output backend (MIDI/OSC connect state, etc.). */ diff --git a/manifold/tests/e2e/output-display-count.spec.ts b/manifold/tests/e2e/output-display-count.spec.ts new file mode 100644 index 0000000..cef0785 --- /dev/null +++ b/manifold/tests/e2e/output-display-count.spec.ts @@ -0,0 +1,28 @@ +import { expect, test } from '@playwright/test'; +import { loadProbe } from './helpers'; +import type { MfDebugHook } from '../../src/console/ConsoleApp'; + +declare global { + interface Window { + __mf?: MfDebugHook; + } +} + +test('output sliders follow the active backend output count', async ({ page }) => { + await loadProbe(page); + + await page.evaluate(() => { + window.__mf!.setOutputMode('midi'); + window.__mf!.setMidiCcCount(3); + }); + await expect(page.getByTestId('output-stage')).toHaveAttribute('data-output-count', '3'); + + await page.evaluate(() => window.__mf!.setMidiCcCount(7)); + await expect(page.getByTestId('output-stage')).toHaveAttribute('data-output-count', '7'); + + const fullCount = await page.evaluate(() => { + window.__mf!.setOutputMode('osc'); + return window.__mf!.paramCount(); + }); + await expect(page.getByTestId('output-stage')).toHaveAttribute('data-output-count', String(fullCount)); +}); diff --git a/manifold/tests/output-display-count.test.ts b/manifold/tests/output-display-count.test.ts new file mode 100644 index 0000000..da77680 --- /dev/null +++ b/manifold/tests/output-display-count.test.ts @@ -0,0 +1,18 @@ +import { expect, test } from 'bun:test'; +import { outputDisplayCount } from '../src/console/output-mode'; + +test('the active MIDI CC count controls the presented output count', () => { + expect(outputDisplayCount('midi', 33, { midi: 8 })).toBe(8); + expect(outputDisplayCount('midi', 33, { midi: 16 })).toBe(16); +}); + +test('the selector supports configured counts for any output mode', () => { + expect(outputDisplayCount('osc', 33, { osc: 5 })).toBe(5); + expect(outputDisplayCount('synth', 33)).toBe(33); +}); + +test('configured output counts are integral and capped by available model outputs', () => { + expect(outputDisplayCount('midi', 12, { midi: 99 })).toBe(12); + expect(outputDisplayCount('midi', 12, { midi: 4.9 })).toBe(4); + expect(outputDisplayCount('midi', 12, { midi: -2 })).toBe(0); +});