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.
This commit is contained in:
2026-07-22 19:55:23 -04:00
parent a9ba532f07
commit 06256f05e9
3 changed files with 580 additions and 0 deletions
+336
View File
@@ -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<ModeId>` 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.