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:
@@ -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.
|
||||
Reference in New Issue
Block a user