Files
reasampler/docs/product/design-view.md
T
daniel 1688de553b docs: settle Design View two-canvas sub-phase (fixed-lane per-item modes)
Item-level mode separation via REAPER fixed lanes; new content auto-tags to
the active mode; inactive lanes hidden and silenced; a managed-vs-manual lane
index keeps toggles off user comp lanes; capture placement mode-aware.
Precision invariants unaffected; REAPER 7 floor.
2026-07-23 16:32:58 -04:00

680 lines
38 KiB
Markdown

# 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). The **two-canvas direction** (item-level
mode separation via fixed lanes) is also settled with Daniel (2026-07-23) — see the
§Two-canvas direction section, which **supersedes** the "not a second canvas"
framing below. Open items for Daniel are listed above that section.
---
## 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 *literal* second canvas.** REAPER has exactly one arrange timeline.
Design View does not create a parallel surface, a separate window, or a duplicate
project — that stays true and is enforced in review. It is the *same timeline* with
a curated, filtered track set and the inactive content parked.
> **Superseded in one respect (2026-07-23).** The original claim here was that
> Design View separates only *tracks*, not *items*. That is now extended: the
> settled §Two-canvas direction adds **item-level** mode separation on shared tracks
> via REAPER fixed lanes — so each stance can own its own items as well as its own
> tracks. It is still not a literal second surface/window/project; it is a mode
> *projection* over the one timeline that now reaches items. Read §Two-canvas
> direction for the settled shape; the sentence "It does not separate items" no
> longer holds.
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's visibility is the
**OR of two sources**: (a) *descendant-derived* — it appears in every mode that
*any* of its descendant leaves belongs to; **or** (b) *its own membership* — an
untagged parent is an Arrange member by default, exactly like a leaf. If a folder
holds two Design leaves and one Arrange leaf, the folder shows in both modes by
descendant derivation. The case own-membership fixes: an **untagged folder that
carries its own FX/media** above leaves that are *all* tagged Design still shows in
**Arrange** (its own default) as well as Design (derived from its children) —
without the own-membership clause it would vanish from Arrange even though the
folder itself is an Arrange member. A parent is **never parked** in any mode it is
visible in. Rule of thumb: **tag leaves; parents follow** — still the common case.
- **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 descendant-derived half of the parent rule is why the pure model must be
**folder-tree aware**. Own-membership (part b) is answerable from the membership
index alone, but descendant derivation (part a) is not — to know whether *any* child
leaf is visible in mode M, the model 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 **every** leaf into **active** (belongs to M, or
show-both) and **inactive** (belongs to some other mode — tagged elsewhere, or
untagged/Arrange when M is not Arrange). Untagged leaves are Arrange members and
park identically to tagged ones; there is no unmanaged leaf. 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 — across every managed leaf, tagged or
untagged. 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_mode_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.
---
# Two-canvas direction — settled (2026-07-23)
> **Status: settled with Daniel (2026-07-23).** This section reopens and supersedes
> the "not a second canvas" decision above. Daniel's stated intent (verbatim): *"I
> wasn't the one who specified single-canvas, that emerged from REAPER's design. I
> want to come as close as possible to supporting two separate design and arrange
> canvases, which are actually the same project but have different items and
> leaves."* The single-canvas framing above was a **derivation from a REAPER
> constraint**, not a product choice — so it was reopened, and Daniel has now signed
> off on the mechanism (fixed item lanes) and resolved all five forks. Everything
> under this heading **supersedes** the "not a second canvas" language in *What it is
> (and what it is not)*: Design View is now a mode projection that reaches **both
> tracks and items**. This is an **additive sub-phase** (Phase D2 / Phase E) on top
> of the shipped track-parking work — not a rewrite of it.
## What actually changed in the ask
The shipped model separates modes by **track membership** on one shared timeline:
tag a leaf into Design, and when you flip to Arrange that leaf is parked — hidden,
out of the mix, FX offline. Because items ride their tracks, a Design track's items
already vanish from the Arrange view. **The "different leaves" half of Daniel's ask
is already delivered.** Flipping modes already gives you a different *track set*.
The genuinely new half is **"different items."** Daniel wants two canvases that
differ not only in which tracks are present but in which *items* are present —
including items that live on a track shared by both stances. Concretely: a track
that exists in both Arrange and Design (a show-both reference track, or a folder
carrying its own media) should be able to hold a design take that only appears in
Design and an arrangement take that only appears in Arrange, on the *same track*,
without the two colliding on the timeline. Track-parking can't express that,
because parking is all-or-nothing per track — it can hide a whole track, never a
subset of its items.
So the reframe is precise: **the feature grows from track-level mode separation to
track-level *plus* item-level mode separation.** "Two canvases" = each stance owns
its own item set, layered over the shared project, with tracks it doesn't want
parked as today.
## The hard constraint (and why it's not fatal)
REAPER has exactly one arrange timeline and one project item graph. There is no
API to instantiate a second arrange surface, and the "no second window/no
duplicate project" observation in the shipped framing is *structurally true* — you
cannot conjure a literal second canvas. What is reversed is only the **conclusion
drawn from it**: that item separation is therefore off the table. It isn't. Items
carry per-item state (`I_FIXEDLANE`, `C_LANEPLAYS`, `D_POSITION`, track
assignment) that the tool can drive to make an item present-or-absent *per mode*
without destroying it. "Two canvases" becomes an **item-visibility projection over
one timeline**, exactly as "two track sets" is a track-visibility projection over
one timeline today. Same discipline, one level deeper.
## Mechanism (settled) and the alternatives it beat
**The settled mechanism is Option A — fixed lanes as per-mode item layers.** The two
alternatives (B timebase-offset, C subproject) are kept below because *why they were
rejected* is load-bearing: both collide with settled invariants, and recording that
keeps them from being re-proposed.
### Option A — Fixed lanes as per-mode item layers (SETTLED)
REAPER's **fixed-lane** feature lets a single track hold multiple horizontal lanes
of items sharing one timebase, with **per-lane play/visibility control**. Map each
mode to a lane: Arrange items live in the Arrange lane, Design items in the Design
lane. Toggling a mode sets `C_LANEPLAYS` so only the active mode's lane plays and
shows; the collapse/hidden flags hide the inactive lane. Items keep their real
positions and their real track — nothing is moved off the timeline, nothing is
deleted.
**SDK surface (verified present in `vendor/reaper-sdk`).** Track-side:
`I_FREEMODE = 2` (fixed-lane mode), `I_NUMFIXEDLANES`, per-lane `C_LANEPLAYS:N`.
Item-side: `I_FIXEDLANE`, `C_LANEPLAYS`, `B_FIXEDLANE_HIDDEN`. Two operational
notes for the implementation spec: **`I_FREEMODE` changes require
`UpdateTimeline()`** to take visible effect, and fixed lanes shipped in **REAPER 7**,
which sets the runtime floor (see fork 5).
- **Fidelity to the two-canvas feel:** high. Same track, same time position, but the
design take and the arrange take are genuinely distinct item sets that never
co-occupy the view. This is exactly "same project, different items."
- **Non-destructiveness:** strong. Lane assignment and `C_LANEPLAYS` are
reversible flags; the item is never relocated in time or deleted. Snapshot the
prior lane state, restore on toggle — a direct extension of the existing
snapshot/restore contract to a new flag family.
- **Fit with park/restore:** clean. It's the same architecture — the planner gains
an item-lane op family alongside the track-flag op family. Pure model stays pure:
lane math (which lane = which mode, which `C_LANEPLAYS` value per mode) is
REAPER-free and unit-testable, mirroring the existing flag planner.
- **Capture-pillar precision:** untouched. Capture writes files to the bank and
never inserts into the timeline; lanes are a placement/view concern downstream of
capture. The null test and bit-identical invariants are unaffected because no
capture path changes.
- **Portability/persistence:** native. Fixed lanes and lane-plays are stored in the
`.rpp` by REAPER itself; the tool only persists which lane maps to which mode
(a small addition to the `"reasampler"` view section).
- **Cost / caveats:** fixed lanes are a real REAPER mode on a track — enabling
`I_FREEMODE=2` changes how that track behaves for the user (lane buttons, comp
behavior), and users unfamiliar with fixed lanes see new UI on managed tracks.
Under the settled **auto-tag** membership rule (see *Membership — settled* below),
a track becomes lane-split when it first needs to hold content for a second mode,
not by a manual "make this a two-canvas track" opt-in. Requires **REAPER 7**, where
fixed lanes shipped — this sets the runtime floor.
### Option B — Timebase-region offset (each mode owns a stretch of the timeline)
Give each mode a disjoint region of the single timeline — Arrange from 0, Design
starting at some large offset (say bar 1000). Items are authored in their mode's
region; toggling a mode scrolls/zooms the arrange view to that region
(`GetSet_ArrangeView2`) and optionally sets the loop/time selection there.
Different items appear because you're *looking at a different part of the timeline*.
- **Fidelity:** medium. It reads as two workspaces if the scroll is snappy, but
they share tracks vertically — a Design track is still a lane on the same track
strip, just at a different horizontal offset. It doesn't give per-track item
layering; a shared track shows both stances' items if you scroll between them.
- **Non-destructiveness:** high (items just have positions), but **positions become
semantically loaded** — an item's `D_POSITION` now encodes its mode. That
collides with the capture pillar's null test, which re-inserts a capture *at its
source position* to verify it nulls. If source positions live in the Design
region, the null-test placement logic has to be region-aware. Friction with a
load-bearing invariant.
- **Fit with park/restore:** awkward. Parking is per-track; this is per-region.
Two orthogonal separation axes that don't compose cleanly.
- **Portability:** fine (just positions + a stored offset), but the offset is a
magic constant that leaks into every position calculation.
- **Verdict:** clever, but it overloads timeline position with mode semantics and
rubs against the capture invariants. Not recommended.
### Option C — Dual sub-projects / project tabs (true separation, heavy)
REAPER supports project tabs and subprojects (a project rendered as an item in a
parent). Model Design as a genuinely separate project surface, imported/rendered
back into Arrange. This is the *only* option that yields a literally separate
canvas.
- **Fidelity:** highest — it *is* two canvases.
- **Cost:** enormous, and it fights the whole premise. "Same project" is Daniel's
explicit constraint; a subproject is a *different* project file with its own
item graph, its own render boundary, and a copy/sync problem between the two. The
capture pillar's "one bank per project, GUID-keyed project identity, travels with
the .rpp" assumption breaks — which project owns the bank? Persistence,
reconcile, and the show-both/derivation model all have to be rethought across a
project boundary. This is a different product.
- **Verdict:** rejected. It satisfies "two canvases" literally by violating "same
project." Daniel asked for *close as possible* within one project, not a second
project.
## The settled shape
**Option A — fixed lanes as per-mode item layers — layered on top of the shipped
track-parking model.** The shipped model already delivers "different leaves"; this
adds "different items" using a native REAPER mechanism that (a) preserves every
precision invariant, (b) extends the existing snapshot/restore architecture rather
than replacing it, (c) keeps the pure/shell split intact (lane math is pure, lane
writes are shell), and (d) persists natively. It is the transplant-a-known-pattern
move: REAPER already solved "multiple item sets, one track, one timebase,
switchable visibility" for comping — we repurpose comping lanes as mode lanes.
The mental model: **modes are a projection, and now the projection reaches items as
well as tracks.** A track can be Arrange-only (parked in Design as today),
Design-only (parked in Arrange as today), or **shared-and-lane-split** (present in
both, but showing each mode its own lane of items). The third case is the new
capability and the concrete meaning of "two canvases."
Phase namespace: **Phase D2 / Phase E** (per the lettered-phase convention for
parallel features) — additive to the shipped D1 track-mode work, not a rewrite.
## Membership — settled (auto-tag by active mode at creation)
The earlier draft floated a manual per-track opt-in ("make this a two-canvas
track"). **That is superseded.** The settled rule, verbatim from Daniel:
> *"Items AND tracks added to the timeline in arrange mode or design mode should be
> tagged as for that mode only. Existing tracks and items default to arrange. If an
> item is added to arrange mode, it does not appear in design mode. If an item is
> added in design mode, it is NOT part of the arrange mode."*
Unpacked into the settled model:
- **Auto-tag at creation, not manual opt-in.** New content — **both new tracks and
new items** — is tagged to whatever mode is **active at the moment it is created**.
There is no separate "convert this track" gesture; membership follows the active
stance automatically.
- **Pre-existing content defaults to Arrange.** Every track and item that existed
before the mode system (or before this sub-phase) is an Arrange member, exactly as
the track-level default already established.
- **Membership is exclusive.** Design-created content never appears in Arrange, and
Arrange-created content never appears in Design — the one exception is the existing
**show-both** escape hatch, which continues to pin a leaf visible across modes.
- **Track level unchanged; item level is the new reach.** At the track level this is
the same tag-to-active-mode rule already settled. What is new: when a **shared**
track (present in both stances — a show-both track, or a folder carrying its own
media) gains an item in Design mode, that item lands in the track's **Design lane**
and is invisible/silent in Arrange, and vice versa. That per-item split is the
fixed-lane mechanism doing its job.
This resolves the old fork 2 (opt-in vs. automatic) in favor of **automatic**, and
it is the through-line that also settles fork 4 (capture placement) below.
## Inactive-mode items — settled (hidden AND silenced)
When you are in one mode, the other mode's items on a shared track are **both hidden
and silenced**: the off-mode lane is set `C_LANEPLAYS = 0` — neither shown nor
played. This matches the exclusive-membership rule (Design-created content is *not
part of* Arrange, so it must not sound there) and the track-level principle already
settled: **flipping modes is a real change, not a cosmetic one.** A user who wants
the design lane audible under the arrangement uses show-both, which is the deliberate
opt-out; the default is full separation.
## Managed vs. manual lanes — settled (2026-07-23)
Fixed lanes are not only the mechanism we borrow for mode-item layering — they are
also **REAPER's native comping surface.** A user may want *their own* multiple lanes
on a track (manual comp takes, alternate reads) that have nothing to do with the mode
system. So a fixed-lane track can host **both** kinds of lane at once, and the tool
must not confuse them. Daniel's constraint, verbatim intent: *"as long as I can still
opt in manually to put multiple lanes on a track. we must index which lanes are
managed and which lanes are manual."*
The settled distinction:
- **Managed lanes** — minted and owned by the mode system to represent a mode's item
set on that track. Mode toggles drive these: the active mode's managed lane shows
and plays (`C_LANEPLAYS`), inactive-mode managed lanes are hidden and silenced.
These are the lanes the two-canvas mechanism creates and controls.
- **Manual lanes** — lanes the user created outside the mode system (native REAPER
comping, alternate takes). The mode system did **not** mint these and **must never
touch them**: a mode toggle never shows, hides, silences, re-lanes, or re-plays a
manual lane. Its `C_LANEPLAYS` state is the user's, left exactly as they set it.
This is the fixed-lane analog of the two invariants already load-bearing in D1 —
*never touch `B_MUTE`/`I_SOLO`* and *never touch the master* — extended to a third
surface: **never drive a lane the tool did not mint.** It is the same non-destructive
promise (the tool owns only what it created) reaching one level deeper, into the lane
dimension.
### Lane-ownership index (the new data)
The tool maintains a **lane-ownership index**: per (track GUID, lane) it records
whether the lane is **managed** (and if so, which mode owns it) or **manual**. This
rides in the `"reasampler"` `view_state` alongside the membership index, GUID-keyed
and portable — same travel-with-the-`.rpp` guarantee as everything else in the view
section. Deciding *which lanes a toggle may touch* (managed only) is a **pure,
testable** query over this index; reading REAPER's live lane state is **shell**. The
seam is the same as everywhere else in the phase.
> **Lane-identity fragility — flag for the implementation spec, not solved here.**
> REAPER's `I_FIXEDLANE` is the lane's identity and is how the ownership index keys to
> a specific lane. Whether `I_FIXEDLANE` values survive lane reorder / renumber /
> deletion without the index going stale is a **design point the technical spec must
> resolve** (the same class of concern as GUID-keyed reorder-safety for tracks). The
> product decision is only that managed and manual lanes are indexed and distinct; how
> the index stays correct under lane reordering is an implementation call, not a
> product one.
### What a mode toggle does to manual lanes (intended behavior)
A mode toggle **leaves every manual lane exactly as the user set it** — its
`C_LANEPLAYS` (play/show) state is untouched across the toggle. Only managed lanes are
partitioned into active (shown/played) and inactive (hidden/silenced). Concretely: if
a shared track has an Arrange managed lane, a Design managed lane, and two manual comp
lanes the user is auditioning, flipping Arrange↔Design swaps which managed lane plays
and leaves both manual lanes precisely as they were.
**Interaction with auto-tag.** Auto-tag applies to content the user adds as **normal
timeline content** — items dropped, recorded, or placed onto the arrange surface in
the active mode. It does **not** reach into a lane the user is hand-managing: items a
user adds to a **manual** lane are **not** auto-tagged to the active mode, because the
user has taken explicit lane control there and the tool respects that boundary — the
manual/managed distinction is exactly the signal that this content is outside the mode
system.
> **Auto-tag/manual-lane boundary heuristic — flag for the implementation spec.**
> Distinguishing "item added as normal timeline content (auto-tag it)" from "item added
> to a manual lane the user hand-manages (leave it alone)" may need a heuristic at the
> detection layer — e.g., an item landing in a lane the ownership index marks manual is
> exempt from auto-tag; an item landing outside any managed lane on a mode-managed
> track follows the active-mode rule. The precise rule is an **open implementation
> design point**, not invented here. The product decision is the boundary itself:
> manual-lane content is off-limits to auto-tag.
## Capture placement — settled (mode-aware)
Daniel's rule is general over *"items added to the timeline."* So an **explicit**
placement while in Design mode — including capture-and-place — lands the item in the
**Design lane**; the same rule governs manual insertion. This is the same auto-tag
rule reaching the placement pillar, not a new special case.
**The capture load-bearing principle is untouched.** Capture still writes a file to
the bank and an index entry and **never** auto-inserts into the timeline. This rule
governs only *where an explicit placement lands* once the user chooses to place —
it does not create any auto-insertion path. The precision invariants (null test,
bit-identical repeats, non-destructive, exact bounds, relative paths) are entirely
unaffected: no capture path changes; lanes are a placement/view concern downstream
of the file.
## REAPER version floor — settled (v7)
Fixed lanes shipped in **REAPER 7**, so this sub-phase sets the runtime floor at
**v7**. The full API surface (track-side `I_FREEMODE=2` / `I_NUMFIXEDLANES` /
`C_LANEPLAYS:N`, item-side `I_FIXEDLANE` / `C_LANEPLAYS` / `B_FIXEDLANE_HIDDEN`) is
**verified present in the vendored SDK header**. No version gate branch is needed —
below v7 the sub-phase is simply unavailable.
## Technical consideration — detecting new content (for the implementation spec)
**REAPER exposes no clean "item added" / "track added" event callback.** The auto-tag
rule therefore requires the shell to detect new track/item GUIDs by **diffing project
state on the panel's existing timer**. The `bank_panel` already polls and fingerprints
the bank; this extends that same machinery to the timeline's tracks and items.
- **Cadence / correctness.** Each poll, the shell compares the live track/item GUID
set against the previous poll's set; **any GUID new since the last poll is tagged to
the then-active mode.** The diffing cadence and the "new-since-last-poll ⇒ tag"
correctness rule are a design point the implementation must handle (e.g., first poll
after project open must not mass-tag pre-existing content — pre-existing defaults to
Arrange, per the membership rule).
- **Pure/shell split.** The **tagging decision** — given a set of new GUIDs plus the
active mode, produce the membership writes — is **pure and unit-testable**, mirroring
the existing planner. The **detection** — diffing REAPER's live item/track set each
timer tick — is **shell**. Keep the seam here: the shell hands the pure model "these
GUIDs are new, the active mode is M," and the model returns the membership mutations.