fix(manifold): preserve output card names on delete

This commit is contained in:
monkey-w1n5t0n 2026-07-25 15:28:26 +02:00
parent 4db0f498eb
commit a7b6230b8b
3 changed files with 28 additions and 6 deletions

View file

@ -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`,

View file

@ -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 (
<OutputControlRow
key={p.id}

View file

@ -45,6 +45,19 @@ test('output cards add/delete in both drawer depths and capacity mode avoids rec
await expect(page.getByTestId('output-stage')).toHaveAttribute('data-output-count', String(fullCount));
});
test('deleting a named particle card removes that identity instead of shifting its name', async ({ page }) => {
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);