diff --git a/manifold/ONBOARDING.md b/manifold/ONBOARDING.md index 601cdd5..1b83dd8 100644 --- a/manifold/ONBOARDING.md +++ b/manifold/ONBOARDING.md @@ -125,7 +125,9 @@ for the narrow pane. boundary for the stage, backend context, and routing rows. MIDI starts with eight cards; every backend can add a card or delete any individual card in condensed and expanded Outputs drawers. The `N outputs` chip always reports that same set. `MFParam.id` is semantic identity; array position - is not. Settings decides whether edits retain spare network capacity or keep exact arity. + is not; backend-specific display names (including Particle System names) resolve from that identity, + so deleting a middle card cannot transfer its name or settings to the next card. Settings decides + whether edits retain spare network capacity or keep exact arity. - `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/Drawers.tsx b/manifold/src/console/Drawers.tsx index 9bc987e..31b222e 100644 --- a/manifold/src/console/Drawers.tsx +++ b/manifold/src/console/Drawers.tsx @@ -26,7 +26,7 @@ import type { ConsoleCtx, DrawerDepth, DrawerKey, FeedbackModeUI, SoloMode } fro import type { InputMode } from '../inputs'; import { OutputControlRow } from '../dock/OutputControlRow'; import { OutputsBackendConfig, BackendStatusChip } from '../dock/OutputsBackendConfig'; -import { shapeValues } from './model'; +import { shapeValues, type MFParam } from './model'; import { outputModeDescriptor } from './output-mode'; import { useSettings, unfocusedIconCss } from '../settings/settings-store'; import type { UnfocusedIconColour, InputMapMode } from '../settings/settings-store'; @@ -734,9 +734,16 @@ function RoutingDrawer(ctx: ConsoleCtx, depth: DrawerDepth) { }, {}); 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; + // Particle labels belong to semantic cards, not their current array slots. + // A deletion compacts the active prefix, so deriving the label from `idx` + // would make the deleted name reappear on its successor. + const particleNamesById = new Map( + ctx.mode.params.map((param, index) => [param.id, VISUAL_NAMES[index] ?? param.name]), + ); + const nameFor = (param: MFParam) => + ctx.outputMode === 'particles' + ? particleNamesById.get(param.id) ?? param.name + : param.name; const expanded = depth === 'expanded'; const rows = expanded ? activeParams : activeParams.slice(0, 6); @@ -771,7 +778,7 @@ function RoutingDrawer(ctx: ConsoleCtx, depth: DrawerDepth) { > {rows.map((p) => { const i = ctx.params.indexOf(p); - const labelled = { ...p, name: nameFor(i, p.name) }; + const labelled = { ...p, name: nameFor(p) }; return ( { + await loadProbe(page); + + await page.getByTitle('Outputs').click(); + await expect(page.getByRole('button', { name: 'Delete Scale output' })).toBeVisible(); + await page.getByRole('button', { name: 'Delete Scale output' }).click(); + + await expect(page.getByText('32 outputs', { exact: true })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Delete Scale output' })).toHaveCount(0); + await expect(page.getByRole('button', { name: 'Delete Speed output' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Delete Hue output' })).toBeVisible(); +}); + test('exact I/O persists and adapts examples across a deleted output identity', async ({ page }) => { await loadProbe(page);