From 06256f05e9e443fe362c6e41a5736fa28d1f2e49 Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Wed, 22 Jul 2026 19:55:23 -0400 Subject: [PATCH] Add Design View phase spec (Phase D), parallel to capture roadmap CONTEXT.md: additive Design View technical spec section. PLAN.md: additive Phase D (D1-D5) milestone breakdown. docs/product/design-view.md: product framing, N-mode reasoning, screenset differentiation, design-direction calls. --- CONTEXT.md | 146 ++++++++++++++++ PLAN.md | 98 +++++++++++ docs/product/design-view.md | 336 ++++++++++++++++++++++++++++++++++++ 3 files changed, 580 insertions(+) create mode 100644 docs/product/design-view.md diff --git a/CONTEXT.md b/CONTEXT.md index eaf7d57..8a188d7 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -162,3 +162,149 @@ Persistence & paths: - Audio format for the bank (wav bit depth default; allow float for wavetable fidelity). - Thumbnail cache: recompute vs store peak bins alongside the index. + +--- + +# Design View — additive phase spec + +> **Additive section.** This is a standalone phase parallel to — not part of — the +> M0–M11 capture roadmap above. Nothing above changes. Product framing (workflow +> narrative, screenset differentiation, N-mode reasoning, design-direction calls) +> lives in `docs/product/design-view.md`; this section is the authoritative +> technical spec, matching the house style of the capture spec. Same standing +> discipline applies: **verify every REAPER API name/signature against +> `vendor/reaper-sdk/sdk/reaper_plugin_functions.h` before use.** + +## What it is + +A **track-visibility-plus-processing "mode" system**, toggled from the ReaSampler +window. Tracks used purely for sound design (scratch oscillators, FX mangling, +resampling sources) are tagged into **Design** mode; the arrangement's real tracks +are **Arrange** mode (the default). Toggling to a mode **hides and disables** the +tracks that don't belong to it. Workflow value: mental separation + clutter +elimination — be in Design view, resample into the bank, flip to Arrange, place it. + +**This is not a separate canvas.** REAPER has exactly one arrange timeline. Design +View is the *same timeline* with a curated, filtered track set and the inactive +tracks parked — not a second surface, window, or duplicated project. + +It is the visibility/processing analog of the capture pillar's load-bearing rule: +**designing and arranging are separate stances on one timeline**, and the tool +enforces the separation **without ever destroying the user's real state.** + +## Settled decisions + +- **Membership.** Default = Arrange; every untagged leaf belongs to it. Leaves + **opt in** to Design (or any mode). No track appears in two modes at once except + (a) via an explicit **show-both** toggle, or (b) parent/folder derivation. +- **Parents are derived, never tagged.** A parent appears in — and is never parked + in — every mode any of its descendant leaves belongs to. Rule of thumb: **tag + leaves; parents follow.** Master track is always visible and never touched. +- **N-mode model, two-mode UI.** The data model carries arbitrarily many modes; the + UI ships **Arrange** + **Design**. A mode is (stable id, display name, ordinal). + Arrange is special only as the default home for untagged leaves. +- **Parking a track** (inactive-mode leaf): `B_SHOWINTCP=0`, `B_SHOWINMIXER=0` + (hide both panels), `B_MAINSEND=0` (out of mix), `I_FXEN=0` (FX bypassed), and + `TrackFX_SetOffline(track, fx, true)` for **each** FX (reclaim CPU). Full CPU-park + is the deliberate choice over mix-removal-only. +- **Never touches `B_MUTE` / `I_SOLO`.** The tool owns only visibility, + `B_MAINSEND`, `I_FXEN`, and per-FX offline — and only on tracks it tagged. User + mute/solo survives every toggle untouched. +- **Persistence.** Membership index + last-active mode + per-track flag snapshots + ride in the existing `"reasampler"` project ext-state namespace and travel with + the `.rpp`. On project open, reapply the active mode's visibility + processing. + +## Precision invariants (enforce, test) + +- **Non-destructive restore.** For every flag the tool drives, snapshot the prior + value **before** parking; on toggle-back restore **from the snapshot**, never to a + hardcoded "on." Round-trip (snapshot → park → restore) returns every driven flag + to its captured value. This is the phase's trust anchor — the analog of the + capture null test — and is enforced in the pure layer. +- **Mute/solo untouched.** No toggle ever reads or writes `B_MUTE` / `I_SOLO`. +- **Master untouched.** The tool never drives the master track's visibility flags + (the SDK forbids `B_SHOWINTCP`/`B_SHOWINMIXER` on master; the invariant agrees). +- **GUID-keyed, reorder-safe.** Membership keys on track GUID (`GetTrackGUID`), + never track index; tolerates unknown/stale GUIDs (prune on reconcile). +- **Relative/portable state only** in the persisted view section (GUID strings, mode + ids — no absolute paths, no index positions). + +## Documented caveat + +Offlined FX **re-instantiate** when a track returns to the active mode. Stateful +plugins (convolution, loaded samplers, tail-holding effects) re-initialize on +return — possible load hitch, un-persisted internal state lost. Accepted cost of +the CPU reclaim; surface it at the toggle affordance (tooltip). + +## Module architecture (preserve the pure/shell split) + +Pure (no REAPER types, unit-tested — the mirror of `bank_model`): +- `view_model` — mode registry (id/name/ordinal; Arrange + Design seeded); + membership index (`track GUID → { mode ids }` + per-track show-both flag; add / + remove / retag / query); **folder-tree-aware** visibility derivation (given the + current parent↔child tree supplied by the shell + the active mode, compute the + visible set); the **parking/restore planner** (given active mode + snapshot + record, emit the exact (track, flag, value) operation lists for park and + restore — where the restore invariant is enforced); JSON round-trip of modes + + membership + show-both + snapshots + active mode. + +REAPER-facing: +- `view` shell — reads `I_FOLDERDEPTH` across the track list to build the + parent↔child tree and feeds it to `view_model`; applies the planner's operations + via `SetMediaTrackInfo_Value` (`B_SHOWINTCP` / `B_SHOWINMIXER` / `B_MAINSEND` / + `I_FXEN`) and `TrackFX_GetCount` + per-FX `TrackFX_SetOffline`; snapshots prior + flag values before parking; resolves GUIDs via + `GetTrackGUID` / `guidToString` / `stringToGuid`. Never touches master + visibility, never touches `B_MUTE` / `I_SOLO`. +- `persist` (slice) — serialize/deserialize the view section into the + `"reasampler"` namespace alongside the bank; on project open, rebuild the tree + and reapply the active mode. +- `actions` (entries) — toggle active mode; activate mode: Arrange / Design; + tag/untag selected tracks → mode; show-both for selected tracks. Registered with + the `command_id` / `gaccel` / `hookcommand` pattern; toggle + mode-jumps + MIDI-bindable. +- UI (in the ReaSampler / bank_panel window) — a **segmented mode switch** + (`[ Arrange | Design ]`) in the window header, active segment lit; small per-mode + membership count; the offlined-FX caveat as a tooltip. Tag/untag acts on the + current REAPER track selection, not a per-track widget. + +## Show-both semantics + +A **per-track "pin visible across modes"** flag that **re-enables processing** +whenever shown. A show-both leaf appears in every mode's visible set and is **never +parked** — its driven flags stay at snapshot/restored values, FX online, in the +mix. ("Show but keep parked" is not offered — a visible-but-silent-and-offline +track is clutter with a thumbnail.) Stored on the membership record; persists; +togglable per selection. + +## REAPER API surface (verify all signatures) + +- Visibility/routing/FX flags via `GetMediaTrackInfo_Value` (snapshot) / + `SetMediaTrackInfo_Value` (apply): `B_SHOWINTCP`, `B_SHOWINMIXER`, `B_MAINSEND`, + `I_FXEN`. (Note: brief cited `B_SHOWINMCP`; verified SDK name is `B_SHOWINMIXER`.) +- Per-FX offline: `TrackFX_GetCount` + `TrackFX_SetOffline(track, fx, offline)`. +- Folder tree: read `I_FOLDERDEPTH` per track to derive parent↔child structure. +- GUID keying: `GetTrackGUID`, `guidToString`, `stringToGuid`. +- Persistence: `SetProjExtState` / `GetProjExtState` under `"reasampler"` (shared + with the bank index — one blob, two logical sections). +- Wrap flag mutations in `Undo_BeginBlock2` / `Undo_EndBlock2` as appropriate. + +## Non-goals / guardrails + +- **No second canvas.** Do not build a parallel arrange surface — reject any such + path in review. +- **Never touch mute/solo.** Any code path reading/writing `B_MUTE` / `I_SOLO` is a + bug. +- **Never touch untagged tracks.** The tool acts only on tracks it tagged (plus + derived parents' *visibility*); it never parks or restores a track it doesn't own. +- **Restore from snapshot, never to a default.** No hardcoded "on" restores. +- **Verify API names** against the SDK header before use. + +## Open questions to resolve during build + +- Snapshot durability across a save-while-parked (persisted here by decision; the + lean alternative is force-restore-to-Arrange on save — see product notes item 4). +- Reconcile behavior when a tagged leaf is deleted or a folder restructured while + parked (ignore-and-prune stale GUIDs on next toggle/open). +- Interaction with the user having a screenset active (Design View drives the same + flags a screenset recall would; last writer wins — confirm no surprising fight). diff --git a/PLAN.md b/PLAN.md index 09320ea..f3696e0 100644 --- a/PLAN.md +++ b/PLAN.md @@ -126,3 +126,101 @@ landed milestone. `int64_t → int` via `static_cast` without a range check; integers that fit in int64 but exceed `INT_MAX` are implementation-defined. Hardening candidate — add bounds check before the cast when integer-field validation is in scope. + +--- + +# Phase D — Design View (parallel to the M0–M11 capture roadmap) + +> **Separate phase namespace.** The M-numbers belong to the capture pillar +> (M0–M11, M4 in flight). Design View is an orthogonal feature on its own track, +> so it uses a **lettered** namespace (D1, D2, …) rather than extending the M-line. +> This keeps the two roadmaps from colliding on numbering and reads correctly: +> Phase D is not "the twelfth capture step," it's a different pillar. Authoritative +> spec: **CONTEXT.md §Design View**. Product framing: `docs/product/design-view.md`. +> When a point lands, doc-keeper moves it to `COMPLETED.md`. + +## D1 — view_model (pure) +**Goal:** REAPER-free mode registry + membership index + folder-tree-aware +visibility derivation + parking/restore planner + JSON round-trip. The heart of the +phase; mirror of `bank_model`. CONTEXT.md §Design View (Module architecture — pure). +**Verify:** CTest green. N-mode model (not a boolean); Arrange + Design seeded. +Restore-planner round-trip (snapshot → park → restore) returns every driven flag to +its captured value. Parent-derivation correct against a supplied folder tree. +JSON round-trip lossless across modes + membership + show-both + snapshots + active +mode. + +- [ ] Mode registry: ordered (id, display name, ordinal); Arrange + Design seeded; + add/query more modes (prove N-mode, not binary). +- [ ] Membership index: `GUID → { mode ids }` + per-track show-both flag; + add / remove / retag / query; untagged = Arrange. +- [ ] Folder-tree-aware visibility derivation: given a supplied parent↔child tree + + active mode, compute the visible set (active leaves, derived-visible parents, + show-both leaves, master always in). +- [ ] Parking/restore planner: emit exact (track, flag, value) op-lists for park and + restore from active mode + snapshot record. +- [ ] JSON round-trip: modes + membership + show-both + snapshots + active mode. +- [ ] Tests: N-mode add/query; parent follows tagged leaf (multi-mode parent); + restore-round-trip returns snapshot values (never hardcoded "on"); show-both leaf + never parked; unknown/stale GUID tolerated; JSON lossless. + +## D2 — view shell (apply flags in the DAW) +**Goal:** Read the folder tree and drive REAPER flags per the planner. +CONTEXT.md §Design View (view shell, REAPER API surface). +**Verify (in DAW):** Toggling active mode hides + parks inactive leaves +(`B_SHOWINTCP`/`B_SHOWINMIXER`/`B_MAINSEND`/`I_FXEN` + per-FX offline) and restores +active ones from snapshot. **Master untouched. `B_MUTE`/`I_SOLO` untouched.** +Untagged tracks untouched. Parents follow their tagged descendants. + +- [ ] Build parent↔child tree from `I_FOLDERDEPTH`; feed to `view_model`. +- [ ] Snapshot prior flag values (`GetMediaTrackInfo_Value`) before parking. +- [ ] Apply park/restore ops (`SetMediaTrackInfo_Value` for the four flags; + `TrackFX_GetCount` + per-FX `TrackFX_SetOffline`). Verify flag names/signatures. +- [ ] GUID resolution: `GetTrackGUID` / `guidToString` / `stringToGuid` (never index). +- [ ] Review gate: no path touches master visibility or `B_MUTE`/`I_SOLO`, or any + untagged track's owned flags. + +## D3 — persist slice (view state ↔ project ext state) +**Goal:** Serialize the view section into the `"reasampler"` namespace alongside the +bank; reapply the active mode on project open. CONTEXT.md §Design View (persist). +**Verify (in DAW):** Membership + active mode + snapshots survive Save / Save As / +close+reopen; on open, the active mode's visibility + processing is reapplied. +Saved-while-parked project restores parked tracks from persisted snapshots (not to a +guessed "on"). + +- [ ] Serialize/deserialize the view section under `"reasampler"` (shared blob, + distinct section from the bank index). +- [ ] Reapply active mode on project open (rebuild tree, run the planner). +- [ ] Confirm survival across Save / Save As; snapshot durability across + save-while-parked. + +## D4 — actions +**Goal:** Bindable action set for the mode workflow. CONTEXT.md §Design View +(actions). **Verify (in DAW):** Each action registered (bindable in Actions list); +toggle + mode-jumps MIDI-bindable; tag/untag acts on the current track selection. + +- [ ] Toggle active mode (cycle; extensible to cycle-all for >2 modes). +- [ ] Activate mode: Arrange / Activate mode: Design (direct jumps). +- [ ] Tag selected tracks → Design / → Arrange; Untag selected (= → Arrange). +- [ ] Show-both for selected tracks (toggle). +- [ ] Register each (`command_id`/`gaccel`/`hookcommand`); toggle + jumps MIDI-bindable. + +## D5 — in-window toggle affordance (UI) +**Goal:** The segmented mode switch in the ReaSampler / bank_panel window header. +CONTEXT.md §Design View (UI). **Verify (in DAW):** Segmented control shows current +mode (lit segment), one click flips modes via the D4 toggle action, per-mode +membership count visible, offlined-FX caveat surfaced as a tooltip. + +- [ ] Segmented mode switch `[ Arrange | Design ]` in the window header; active lit. +- [ ] Wire the switch to the toggle/activate actions from D4. +- [ ] Per-mode membership count display. +- [ ] Offlined-FX re-init caveat as a tooltip on the switch. + +## Phase D open questions +- **Snapshot durability across save-while-parked** — persisted here by decision; + lean alternative is force-restore-to-Arrange on save (product notes item 4). + (touches D3) +- **Reconcile on delete/restructure** — tagged leaf deleted or folder restructured + while parked leaves a stale GUID; ignore-and-prune on next toggle/open. + (touches D1/D2) +- **Screenset coexistence** — Design View drives the same flags a screenset recall + would; confirm last-writer-wins is not surprising. (touches D2) diff --git a/docs/product/design-view.md b/docs/product/design-view.md new file mode 100644 index 0000000..af2bd5b --- /dev/null +++ b/docs/product/design-view.md @@ -0,0 +1,336 @@ +# Design View — product notes + +Framing, rationale, and design-direction calls behind the **Design View** phase. +The tickable spec lives in `PLAN.md` (Phase D) and the authoritative technical +detail in `CONTEXT.md` (§Design View). This doc holds the *why* — the workflow +narrative, the N-mode reasoning, the screenset differentiation, and the +design-direction recommendations — so those don't clutter the build docs. + +Status: settled with Daniel (2026-07-22). Open items for Daniel are listed at the +bottom. + +--- + +## What it is (and what it is not) + +**Design View is a track-visibility-plus-processing "mode" system.** Tracks used +purely for sound design — scratch oscillators, FX-mangling chains, resampling +sources — get tagged into **Design** mode. The arrangement's real tracks stay in +**Arrange** mode (the default). Toggling to a mode *hides and disables* the tracks +that don't belong to it. + +The value is **mental separation plus clutter elimination**, tuned to the resample +workflow: be in Design view, resample something into the bank, flip to Arrange, +place it. The design scaffolding never touches the arrangement while you're +arranging, and the arrangement never buries the design bench while you're +designing. + +**It is not a second canvas.** REAPER has exactly one arrange timeline. Design +View does not create a parallel surface, a separate window, or a duplicate +project. It is the *same timeline* with a curated, filtered track set and the +inactive tracks parked. Anyone expecting a second arrange view will be +disappointed — and that expectation must be headed off in the spec, not +discovered in review. + +This is the visibility/processing analog of the capture pillar's load-bearing +rule. The capture pillar says *capture and placement are separate acts*. Design +View says *designing and arranging are separate stances on one timeline* — and the +tool enforces the separation without ever destroying the user's real state. + +--- + +## Why not REAPER screensets? (pre-answered) + +REAPER already ships screensets, which save and recall track visibility. This is +deliberately different on three axes: + +1. **Live membership, not a flat snapshot.** A screenset is a re-saved picture of + "these tracks visible, those hidden" — you re-capture it every time the track + set changes. Design View is a *membership index*: tag a track into a mode once, + and it is handled forever, including tracks added later. You maintain + membership, not snapshots. +2. **It disables processing, not just visibility.** Screensets hide tracks; the + hidden tracks still run their FX and still feed the mix. Design View parks the + inactive mode's tracks: out of the mix, FX bypassed, FX taken offline to + reclaim CPU. Flipping modes is a real load change, not a cosmetic one. +3. **It is integrated with the resample workflow.** The toggle lives in the + ReaSampler window, next to the bank, driven by the same actions that capture + and place. It is part of the tool's loop, not a general window-management + feature parked in a menu. + +--- + +## N-mode reasoning (why the model is not a boolean) + +Daniel's instruction was explicit: design the data model as **N-mode**, ship the +UI with two. "Design Mode, or any other future mode." + +The cost of a boolean is a **backfill cliff**: the moment a third stance appears +("Mixdown" that hides everything but buses; "Print" that isolates a stem chain), +a boolean model forces a migration of the persisted index and a rewrite of every +call site that assumed two states. The cost of N-mode *now* is trivial: a mode is +an identifier, membership is a `GUID → set` relation instead of a +`GUID → bool`. The visibility/parking logic is already "is this track in the +active mode?" — which is a set-membership test regardless of how many modes exist. + +So: **the model carries arbitrarily many modes; the UI exposes two.** Adding a +third mode later is a UI-and-naming task, not a data-migration task. This is the +defer-the-feature, design-the-seam principle applied. + +A mode is a small record: a stable id, a display name, and an ordinal for tab +order. Two seed modes ship: **Arrange** (the implicit default; every untagged leaf +belongs to it) and **Design**. Arrange is special only in that it is the fallback +home for untagged leaves — structurally it is just another mode. + +--- + +## Membership model (the settled rules, in prose) + +- **Default is Arrange.** Every leaf track belongs to Arrange unless it opts in + elsewhere. Untagged = Arrange. +- **Leaves opt in.** You tag a *leaf* track into Design (or any mode). You never + tag a parent. +- **Parents are derived, never tagged.** A folder/parent track appears in — and is + never parked in — every mode that *any* of its descendant leaves belongs to. If + a folder holds two Design leaves and one Arrange leaf, the folder shows in both + modes. Rule of thumb: **tag leaves; parents follow.** +- **Master is always visible**, in every mode, and the tool never touches its + flags. (REAPER's own SDK forbids driving `B_SHOWINTCP`/`B_SHOWINMIXER` on the + master track — the invariant and the API agree.) +- **One mode at a time, with two exceptions:** a leaf can appear in more than one + mode only via (a) the explicit *show-both* toggle, or (b) the parent-derivation + rule above. Otherwise a leaf lives in exactly one mode. + +The parent-derivation rule is why the pure model must be **folder-tree aware**. It +cannot answer "should this parent be visible in mode M?" from the membership index +alone — it needs the current parent↔child structure, which the shell reads from +REAPER's `I_FOLDERDEPTH` and feeds in. The tree is an *input* to the pure model's +visibility query, not something the model stores (folder structure is REAPER's +truth and changes underneath us). + +--- + +## What a toggle actually does (the parking contract) + +Toggling to mode M partitions tagged leaves into **active** (in M, or show-both) +and **inactive** (tagged into some other mode, not M). For each track: + +**Active leaves** and **derived-visible parents** → restored to their snapshot +values (see restore contract) and shown. + +**Inactive leaves** → parked: +- `B_SHOWINTCP = 0` and `B_SHOWINMIXER = 0` — hidden in both panels. +- `B_MAINSEND = 0` — removed from the mix (no audio to parent). +- `I_FXEN = 0` — FX chain bypassed. +- `TrackFX_SetOffline(track, fx, true)` for **each** FX — taken offline to reclaim + CPU. + +Daniel chose the **full CPU-park** option over mix-removal-only, deliberately. The +point of Design mode is a heavy FX bench you don't want taxing the CPU while you +arrange, and vice versa. Half-parking (mix out, FX still resident) would leave the +tax in place. + +**Documented caveat:** offlined FX re-instantiate when the track returns to the +active mode. Stateful plugins (convolution reverbs, samplers with loaded content, +anything holding a tail or a big buffer) re-initialize on return — there may be a +load hitch and any un-persisted internal state is lost. This is an accepted cost +of the CPU reclaim, not a bug. It must be documented at the toggle affordance so +the user isn't surprised. + +**Never touched:** `B_MUTE` and `I_SOLO`. The tool owns visibility, `B_MAINSEND`, +`I_FXEN`, and FX-offline — nothing else — and only on tracks it has tagged. The +user's mute/solo survives every toggle, untouched. This is the exact analog of the +capture pillar's non-destructive invariant: **the tool never destroys the user's +real state to do its job.** + +--- + +## Non-destructive restore contract (the testable invariant) + +For every flag the tool drives on a track, it **snapshots the prior value before +parking** and **restores exactly on toggle-back** — restore *from the snapshot*, +never to a hardcoded "on." If the user had `B_MAINSEND = 0` on a track for their +own reasons before it was parked, it comes back as 0, not 1. + +While a track is parked/hidden, its tool-owned flags (`B_SHOWINTCP`, +`B_SHOWINMIXER`, `B_MAINSEND`, `I_FXEN`, per-FX offline) are **tool-owned**: the +user is not expected to hand-edit a hidden track's routing mid-mode, and the +contract does not promise to merge concurrent hand-edits of a hidden track. The +snapshot is the source of truth for restore. + +This is enforceable and testable in the pure layer: given a snapshot record and a +target mode, the model produces the exact set of (track, flag, value) restore +operations, and a round-trip (snapshot → park → restore) returns every driven flag +to its captured value. The shell's job is only to apply those operations to +REAPER; the *decision* of what to restore to is pure and unit-tested — mirror of +how `bank_model` owns the index logic and the shell only touches the DAW. + +--- + +## Persistence + +The mode-membership index and the last-active mode ride in the existing +`"reasampler"` project ext-state namespace and travel with the `.rpp`. On project +open, the tool reapplies the active mode's visibility + processing state. Same +namespace, same travel-with-the-project guarantee as the bank index — one +`"reasampler"` blob, two logical sections (bank + view). + +Snapshots (prior flag values for currently-parked tracks) also persist: if a +project is saved while in Design mode with Arrange tracks parked, reopening it must +be able to restore those Arrange tracks correctly later. A saved-while-parked +project that lost its snapshots would restore parked tracks to a guessed "on" — +violating the restore contract across a save boundary. + +--- + +## Design-direction recommendations (opinionated) + +### Naming: keep "Design View" as the feature; name modes for *stances*, not *tracks* + +The feature is **Design View**. The modes are **Arrange** and **Design** — verbs- +as-stances the user adopts, not labels for what's in them. "Arrange" and "Design" +read as *what you are doing right now*, which is the whole mental-separation pitch. +Resist "Main / Scratch" or "A / B" — they describe the tracks, not the stance, and +they don't scale to a named third mode. When a third mode arrives it gets a stance +name too ("Mixdown," "Print"). + +### The toggle affordance: a segmented mode switch, not a checkbox + +Recommendation: a **segmented control** (pill/tab switch) in the ReaSampler window +header — `[ Arrange | Design ]` — with the active segment lit. Not a checkbox, not +a menu item as the primary affordance. + +Why segmented over the alternatives: +- **vs. a checkbox** ("Design mode on/off"): a checkbox hardcodes the binary the + data model is explicitly built to avoid. A segmented control grows to + `[ Arrange | Design | Mixdown ]` with no redesign — the UI mirrors the N-mode + model. Borrowed from DAW/plugin A/B/C compare switches and browser device-toolbar + responsive-mode pickers, both of which are "pick one active view from a small + set." +- **vs. a menu**: a mode switch is a *frequent, glanceable* action in this + workflow — you flip it many times a session. It wants to be always-visible and + one-click, showing current state at rest. A menu hides both the state and the + switch. + +The segment also carries the current-mode indicator "for free" (the lit segment is +the state), and it is the natural home for the offlined-FX caveat as a tooltip +("switching parks the other mode's FX offline; stateful plugins re-init on +return"). + +Actions (below) drive the same toggle for keyboard/MIDI binding; the segmented +control is the visible, discoverable surface over those actions. + +### Tagging affordance: act on the track selection, not a per-track widget + +Tag/untag operates on the **current REAPER track selection** via an action (and a +context entry), not a per-track button in ReaSampler's window. "Select your design +tracks, hit *Tag selected → Design*." This matches how a user thinks ("these +tracks are my design bench") and avoids ReaSampler having to render a full track +list mirror. The bank_panel window shows a small membership summary (counts per +mode) rather than a per-track control surface. + +--- + +## Action set (proposed) + +Stable command-id strings follow the sampler family prefix chosen in M0. Default +names below; all bindable, the toggle and mode-jumps also MIDI-bindable to suit the +performance-adjacent workflow. + +- **Toggle active mode** — cycle Arrange ↔ Design (extensible to cycle-through-all + when >2 modes). The segmented control's click target. +- **Activate mode: Arrange** / **Activate mode: Design** — jump directly to a named + mode (distinct from cycle; needed once there are >2, and nicer for bindings). +- **Tag selected tracks → Design** / **Tag selected tracks → Arrange** — move the + current selection's leaves into a mode. (Arrange = untag from any other mode.) +- **Untag selected tracks** — return selection to the default (Arrange), explicit + alias of "Tag → Arrange" for discoverability. +- **Show both for selected tracks** (toggle) — see *show-both semantics* below. + +### Show-both semantics (resolved) + +**Show-both is a per-track "pin visible across modes" flag, and it re-enables +processing whenever the track is shown.** A show-both leaf: +- appears in **every** mode's visible set (like a derived parent, but by explicit + request on a leaf); +- is **never parked** — its driven flags stay at their snapshot/restored values in + all modes, FX online, in the mix. + +Rationale: the use case is a track you need audible in *both* stances — a reference +oscillator you design against and also hear in the arrangement, or a master-bus FX +chain living on a normal track. "Show but keep parked" is not a real need (a track +you can see but that's silent and offline is just clutter with a thumbnail), so +show-both means show-*and-run*. It is the deliberate escape hatch from +one-mode-at-a-time, and it costs CPU by design — that's the user's explicit call +per track. + +Show-both is stored on the membership record for the track, persists with the +index, and is togglable per selection. + +--- + +## Module breakdown (pure / shell / persist / actions / UI) + +Mirrors the capture pillar's split exactly. + +**Pure `view_model` (REAPER-free, unit-tested — the mirror of `bank_model`):** +- Mode registry: ordered set of modes (id, display name, ordinal); Arrange + Design + seeded; add/query more. +- Membership index: `track GUID → { mode ids }` (normally one; multiple only via + show-both), plus the per-track show-both flag. add / remove / retag / query. +- Folder-tree-aware visibility derivation: given the current parent↔child tree + (supplied by the shell) and the active mode, compute the visible set — active + leaves, derived-visible parents, show-both leaves, master always in. +- Parking/restore planner: given the active mode + a snapshot record, emit the + exact (track, flag, value) operation lists for park and for restore. Pure and + fully testable — this is where the restore-contract invariant is enforced. +- JSON round-trip of modes + membership + show-both + snapshots + active mode. + +**REAPER-facing `view` shell:** +- Reads `I_FOLDERDEPTH` across the track list to build the parent↔child tree; feeds + it to the pure model. +- Applies the planner's operations: `SetMediaTrackInfo_Value` for `B_SHOWINTCP` / + `B_SHOWINMIXER` / `B_MAINSEND` / `I_FXEN`; `TrackFX_GetCount` + loop + `TrackFX_SetOffline` per FX. +- Snapshots prior flag values before parking (reads the same flags it will drive). +- Resolves track GUIDs via `GetTrackGUID` / `guidToString` / `stringToGuid` for the + index; never uses track index (unstable across reorders). +- Never touches the master track's visibility flags; never touches `B_MUTE` / + `I_SOLO` on anything. + +**`persist` slice:** +- Serialize/deserialize the view section (modes + membership + show-both + snapshots + + active mode) into the `"reasampler"` ext-state namespace alongside the bank. +- On project open, rebuild the tree, reapply the active mode. + +**`actions` entries:** the set listed above, registered with the +`command_id` / `gaccel` / `hookcommand` pattern; toggle + mode-jumps MIDI-bindable. + +**UI (in the ReaSampler / bank_panel window):** the segmented mode switch in the +window header; a small per-mode membership count; the offlined-FX caveat as a +tooltip on the switch. + +--- + +## Open items for Daniel + +1. **Product-notes location.** This doc was created at `docs/product/` — the first + product-notes directory in the repo. If notes belong elsewhere (`notes/`, + `docs/design/`), say so and it moves. Flagging because establishing the + directory is a project convention, not mine to assume silently. +2. **Phase namespace.** Proposed **Phase D** (a lettered namespace) rather than + M12 — see PLAN.md rationale. Confirm you're happy with letters for parallel, + non-capture phases. +3. **SDK name correction.** The brief cited `B_SHOWINMCP`; the verified SDK flag + for mixer-panel visibility is **`B_SHOWINMIXER`** (SDK header line 2235). The + spec uses the verified name. Just flagging the discrepancy so it isn't a + surprise in review. +4. **Snapshot-across-save durability.** I ruled that per-track snapshots must + persist so a saved-while-parked project restores correctly. This adds snapshot + data to the persisted blob. If you'd rather keep the persisted state lean and + accept that saving-while-in-Design-mode is an edge case (e.g., force-restore-to- + Arrange on save), that's a viable smaller alternative — flag if you prefer it. +5. **Reorder/delete robustness (deferred, not blocking).** If a tagged leaf is + deleted or a folder is restructured while parked, the index holds a stale GUID. + The pure model should tolerate unknown GUIDs (ignore-and-prune on next reconcile) + — noted as a hardening item for the build, not a phase-defining decision.