fix(manifold): sync output sliders with backend count
This commit is contained in:
parent
ec3118004d
commit
f75f683f40
8 changed files with 113 additions and 24 deletions
|
|
@ -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`,
|
||||
|
|
|
|||
|
|
@ -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<string | null>(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)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -876,7 +893,7 @@ export function ConsoleApp() {
|
|||
borderLeft: '1px solid var(--line)',
|
||||
}}
|
||||
>
|
||||
<OutputStage params={params} values={values} onChange={setParam} compact />
|
||||
<OutputStage params={displayedParams} values={displayedValues} onChange={setParam} compact />
|
||||
</div>
|
||||
</div>
|
||||
) : 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}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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<Record<string, number>>((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<Record<string, number>>((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 (
|
||||
<>
|
||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
|
|
@ -656,8 +657,8 @@ function RoutingDrawer(ctx: ConsoleCtx, depth: DrawerDepth) {
|
|||
);
|
||||
})}
|
||||
</div>
|
||||
{!expanded && ctx.params.length > 6 && (
|
||||
<span style={{ fontSize: 9, color: 'var(--fg-dim)' }}>+{ctx.params.length - 6} more — expand to edit</span>
|
||||
{!expanded && activeParams.length > 6 && (
|
||||
<span style={{ fontSize: 9, color: 'var(--fg-dim)' }}>+{activeParams.length - 6} more — expand to edit</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -76,6 +76,8 @@ export function OutputStage({ params, values, onChange, compact = false }: Outpu
|
|||
|
||||
return (
|
||||
<div
|
||||
data-testid="output-stage"
|
||||
data-output-count={params.length}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
|
|
|
|||
|
|
@ -80,3 +80,22 @@ export const DEFAULT_OUTPUT_MODE: OutputMode = OUTPUT_MODES[0].id;
|
|||
export function outputModeDescriptor(id: OutputMode): OutputModeDescriptor {
|
||||
return OUTPUT_MODES.find((m) => 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<Record<OutputMode, number>> = {},
|
||||
): 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)));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<MFParam>) => 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.). */
|
||||
|
|
|
|||
28
manifold/tests/e2e/output-display-count.spec.ts
Normal file
28
manifold/tests/e2e/output-display-count.spec.ts
Normal file
|
|
@ -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));
|
||||
});
|
||||
18
manifold/tests/output-display-count.test.ts
Normal file
18
manifold/tests/output-display-count.test.ts
Normal file
|
|
@ -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);
|
||||
});
|
||||
Loading…
Reference in a new issue