docs(product): reframe S10 capture-first (browser + single-capture setup); reverse S4 auto-select; settle S-NAME-1 filename rename

This commit is contained in:
2026-07-26 19:49:03 -04:00
parent 5de2183ce3
commit 991c190bb8
3 changed files with 312 additions and 102 deletions
+143 -55
View File
@@ -399,48 +399,107 @@ alongside S8.
> `docs/product/visual-design-language.md` (all on `dev`). S10S13 below build with the
> current drawing and **adopt the L1 kit when it lands — not gated on Phase L.**
## S10 — direct-manipulation keymap editor ("ReaSampler 9000" UX overhaul, part 1)
**Goal:** Replace the ±1 nudge-button zone editor with a **piano-keyboard-strip zone
editor** where the keymap is edited by direct manipulation: a horizontal keyboard span
along the top, each zone drawn as a bar over the keys it covers; **drag a zone's edge to
change its low/high note, drag the bar's body to move the whole zone, click a key (or the
zone's root marker) to set the root note.** No number is ever set by clicking a button 40
times. This is the friction Daniel feels every test pass — the current row of seven
mini-buttons per zone is the catastrophe. All layout/hit-test math is a **new pure
geometry module** (`keyboard_strip` — mirror of `mode_switch`/`editor_geometry`); the LICE
draw + drag-state machine is the editor shell. RT discipline untouched (edits still commit
off-thread via `commitMapAndReload`). CONTEXT.md §Phase S (ReaSampler 9000 UX — keymap
editor).
**Verify (in DAW):** a zone's range is set by **dragging its edges on the keyboard strip**
(not by nudge clicks); dragging a zone's body moves the whole range preserving its span;
clicking a key sets/relocates the zone's root (with the repitched result audible on the
next held note); overlapping zones render legibly and resolve first-match as before;
zone add/select/delete still work; the pure geometry module is CTest-green (edge-grab
regions, body-drag delta, key→note mapping, root hit-test) with no host types at its
boundary; the ±1 nudge-button row is gone.
**Depends on:** S5 (the `PerformanceMap`/zone model it edits), S1 (the LICE `IPlugView`
drag/event routing — extends the click-only `wndProc` to `WM_MOUSEMOVE`/`WM_LBUTTONUP`).
**Adopts the Phase L kit when available — not gated on Phase L.** S10 builds its keyboard
strip + zone bars with the current LICE drawing; when Phase L's L1 kit lands on `dev`, this
surface adopts it (the one source of drawing). The drag machine's `WM_MOUSEMOVE` tracking
also lights the kit's **hover** states at near-zero marginal cost once the kit is present.
## S10 — capture-first editor: browser + guided single-capture setup ("ReaSampler 9000" UX overhaul, part 1)
**Goal (REVISED 2026-07-26 — workflow-first reframe, Daniel):** Rebuild the editor's
default face around the **primary flow = one capture, fast**, not a keymap. A giant list of
"item" blocks is visually useless; most instances play a *single capture*, and zones are a
nice-to-have. So the default view is a **capture browser** (scannable cards with peak
thumbnails, name, root/key badge; **bank filter**) feeding a **guided single-capture setup**
(root note, play-mode basics, level) — and the keyboard strip serves the *single-capture*
case first (shows where the capture sits / its root). **Time-to-first-note is the metric.**
Multi-zone keymap editing is **demoted to an opt-in "Zones" panel** (S10-Z below), not the
default. The keyboard-strip drag machinery is still built here, but in service of the
capture-first layout. All layout/hit-test math is **pure geometry** (new `keyboard_strip` +
a `capture_browser` layout module — mirrors of `mode_switch`/`editor_geometry`); the LICE
draw + drag-state machine is the editor shell. RT discipline untouched (edits commit
off-thread via `commitMapAndReload`); the instrument stays a **read-only bank consumer**.
CONTEXT.md §Phase S (ReaSampler 9000 UX — capture-first editor).
- [ ] Pure `keyboard_strip` geometry module: map a MIDI key span across a strip width
(128 keys → pixels, reusing the S6 `embed_strip` key-span idiom); per-zone bar rect from
`[lowNote,highNote]`; **edge-grab hit regions** (a few px inside each end = resize handles,
the middle = move handle); `pixel→note` and a `keyAtPoint` for click-to-set-root; a
drag-delta resolver `(grabbedField, startNote, dxPixels) → newNote`. No VST3/REAPER/LICE
types at the boundary; unit-tested outside the DAW (edge grabs, body-move delta, key
mapping, clamps low≤high, boundary rounding). Mirror of `mode_switch`/`editor_geometry`.
- [ ] Editor shell: draw the keyboard strip + zone bars in LICE (house palette), and drive
a **drag-state machine**`WM_LBUTTONDOWN` grabs an edge/body/key, `WM_MOUSEMOVE`
updates the in-flight zone against the pure resolver, `WM_LBUTTONUP` commits via the
existing `commitMapAndReload` (off-thread reload; RT path untouched). Live visual feedback
while dragging; a single undo-coherent edit on release.
- [ ] Click-to-set-root + zone move/resize replace the seven per-row nudge/delete
mini-buttons; delete stays as one affordance (a small × on the zone bar or a keystroke).
The `zoneHitTest`/±1 nudge path in `editor_geometry` is retired for the new strip (kept
only if a numeric fallback is wanted for accessibility — build-time residual, not a fork).
**Policy reversal — fresh instance is SILENT, nothing auto-selected (was S4).** The S4
"first sample plays" fallback is **removed**: on open with no stored selection, the
instrument plays **nothing** and the editor shows a clear **empty state** ("pick a capture")
— it does not auto-play sample #1. Retires the `selectSample` first-sample fallback
(`sample_map.cpp` "No stored id → fall back to the FIRST sample") and the processor's
Tier-0 fallback that resolved it; an empty stored id now resolves to silence. A capture is
loaded when the user picks one (or via S13 drop-to-load / S8 ingest). This is a deliberate
reversal of the S4 convenience default, not a regression.
**Verify (in DAW):** a fresh instance plays **nothing** and shows the "pick a capture"
empty state (no auto-play of sample #1); the capture browser draws **peak thumbnails**
(the `Sample` peaks bank_model already carries — same data the dock panel thumbnails use),
name, and a root/key badge where present, and is **filterable by bank** (bank_book named
banks); picking a capture loads it, shows it (waveform/peaks + its root on the keyboard
strip), and it plays repitched from its root; time-to-first-note is a pick-then-play, not a
list-scroll; the keyboard strip shows the single capture's root and is draggable to set it;
the pure geometry modules are CTest-green (browser card/grid layout + hit-test; strip
edge-grab/body-move/key→note) with no host types at their boundary; the ±1 nudge-button row
is gone.
**Depends on:** S4 (the selection state + reload path this reverses the fallback on), S1
(the LICE `IPlugView` drag/event routing — extends the click-only `wndProc` to
`WM_MOUSEMOVE`/`WM_LBUTTONUP`), S5 (the `PerformanceMap`/zone model the opt-in Zones panel
edits — but the default face does not require a keymap). **Adopts the Phase L kit when
available — not gated on Phase L.** S10 builds its browser cards + keyboard strip with the
current LICE drawing; when Phase L's L1 kit lands on `dev`, this surface adopts it (the one
source of drawing). The drag machine's `WM_MOUSEMOVE` tracking also lights the kit's
**hover** states at near-zero marginal cost once the kit is present.
- [ ] No-auto-select + empty state (the policy reversal): remove the `selectSample`
first-sample fallback (`sample_map.cpp`) and the processor's Tier-0 fallback that
consumed it — an empty stored selection resolves to **silence**, not sample #1. The
editor draws a clear **empty state** ("pick a capture" affordance) when nothing is
selected. Pure change is testable (empty id → `nullopt`); the empty-state draw is shell.
- [ ] Capture browser (pure layout + shell draw): grow `SampleChoice` to carry the
**peak thumbnail data** (from the `Sample` peaks bank_model already stores — the same
peaks the dock panel draws), the **root/key badge** (S2 `rootNote` intrinsic / the
optional musical key), and its bank. A new pure `capture_browser` module lays out
scannable **cards/rows** (card rect grid, thumbnail rect, hit-test a point → card) — no
host types at the boundary, unit-tested. The shell draws each card's peak thumbnail +
name + badge in LICE (house palette) and routes a click to select.
- [ ] Bank filter (pure + shell): a filter/tab strip over the browser that narrows the
drawn cards to a chosen bank_book bank (or "all"). Filter-tab layout + hit-test pure
(mirror of `mode_switch`); the active-filter state is transient UI state; the shell draws
the tabs and applies the filter to the card list. (Type-to-filter search folds in from
S12 — see S12's boundary note; a name-substring filter over the same card list.)
- [ ] Guided single-capture setup (the fast path): once a capture is picked, a prominent,
self-explanatory setup surface — **root note** (settable on the keyboard strip / typed),
**play-mode basics**, **level** — sized for the single-capture case, not a zone table.
Graphic and descriptive; the point is to get from pick → set → play with no hunting.
- [ ] Pure `keyboard_strip` geometry module (serves the single-capture case first): map a
MIDI key span across a strip width (128 keys → pixels, reusing the S6 `embed_strip`
key-span idiom); a **root marker** for the loaded capture; `pixel→note` and a `keyAtPoint`
for click-to-set-root; a drag-delta resolver `(grabbedField, startNote, dxPixels) →
newNote`; **per-zone bar rect** + **edge-grab hit regions** (resize handles vs. body
move-handle) for the opt-in Zones panel. No VST3/REAPER/LICE types at the boundary;
unit-tested (root marker, edge grabs, body-move delta, key mapping, clamps low≤high,
boundary rounding). Mirror of `mode_switch`/`editor_geometry`.
- [ ] Editor shell drag-state machine: `WM_LBUTTONDOWN` grabs a card / a key / a zone
edge-or-body, `WM_MOUSEMOVE` updates the in-flight edit against the pure resolver,
`WM_LBUTTONUP` commits via the existing `commitMapAndReload` (off-thread reload; RT path
untouched). Live visual feedback while dragging; a single undo-coherent edit on release.
### S10-Z — Zones panel (opt-in multi-zone keymap editing; demoted from the default face)
The multi-zone keymap editor is now an **opt-in view/panel** ("Zones" toggle), not the
default. It reuses the same `keyboard_strip` geometry and drag-state machine: each zone a
bar over the keys it covers; **drag an edge** → low/high note; **drag the bar body** → move
the zone (span preserved); **click a key** → set/relocate the zone's root. This is the
capability RS5K structurally lacks (multi-zone in one instrument), kept as a *nice-to-have*
per Daniel's hierarchy — "most of the time the zones won't be used." Add/select/delete a
zone; overlapping zones render legibly and resolve first-match. The seven ±1 nudge/delete
mini-buttons are retired everywhere; delete is one affordance (a small × on the bar or a
keystroke). The `zoneHitTest`/±1 nudge path in `editor_geometry` is retired (a numeric
fallback for accessibility is a build-time residual, not a fork).
**Verify (in DAW):** the Zones panel is reachable via an explicit toggle (default view is
the capture browser + single-capture setup, not this); a zone's range is set by **dragging
edges** (not nudge clicks); body-drag moves the span; click-a-key sets the root (audible on
the next held note); zone add/select/delete work; the ±1 nudge row is gone.
- [ ] "Zones" panel toggle (opt-in): the default editor face is the capture browser +
single-capture setup; a toggle reveals the multi-zone keymap editor. Toggle state is
transient UI state (or per-instance component state if it should persist — build-time
residual).
- [ ] Zone edit via the shared strip: draw the keyboard strip + zone bars in LICE, drive
the shared drag-state machine (edge = resize, body = move, key = root), commit via
`commitMapAndReload`. Zone add/select/delete as single affordances; ±1 nudge row gone.
## S11 — waveform view with draggable loop points (UX overhaul, part 2)
**Goal:** Give each sample/zone a **waveform display** with **draggable start/end/loop
@@ -464,6 +523,13 @@ CTest-green (px↔frame mapping, marker grab regions, clamp start≤end).
(the zone the loop attaches to), S10 (shares the editor's drag-state machine + shell). The
zero-crossing snap is a small pure helper over the decoded PCM.
> **Boundary note (S10 reframe, 2026-07-26):** the waveform view is now **central to the
> single-capture fast path**, not just per-zone. Selecting a capture in S10's browser shows
> its waveform (this is "see it" in pick → see it → play it); the loop-marker drag here
> extends that same waveform surface. S11's waveform draw is the same one S10's picked-
> capture view uses — build it once, S10 shows it read-only for the single capture, S11 adds
> the draggable loop markers. No renumber; S11 stays the loop-editing point.
- [ ] Pure waveform/marker geometry: `frame↔pixel` mapping across the waveform rect, marker
x-position from a frame index, marker grab regions (start/end/loop-start/loop-end),
drag-delta `(grabbedMarker, dxPixels) → newFrame` with clamps (start≤end, in-bounds). A
@@ -495,14 +561,26 @@ sample is reachable; typing filters the list to matching names; a zone's low/hig
be **typed** (not only dragged) via a click-to-edit field; the amp envelope's ADSR is
**adjustable** (four draggable controls) and the change is audible + persists across project
save/reopen (component state); the scroll/search/slider geometry is CTest-green.
**Depends on:** S10 (the editor shell + drag-state machine), S3 (the `AdsrParams` the ADSR
sliders drive — already wired into the voice engine; today they are fixed defaults), S5
(the map the numeric fields edit).
**Depends on:** S10 (the editor shell + drag-state machine + the **capture browser** the
scroll/search now apply to), S3 (the `AdsrParams` the ADSR sliders drive — already wired
into the voice engine; today they are fixed defaults), S5 (the map the numeric fields edit).
- [ ] Scrollable, searchable sample list: a scroll offset (wheel + scrollbar drag) so long
banks are fully reachable; a type-to-filter search that narrows the drawn rows to matching
display names. Scroll/filter layout + hit-test is pure geometry (visible-row window,
scrollbar thumb rect, search-box rect); filter/scroll state is transient UI state.
> **Boundary note (S10 reframe, 2026-07-26):** the "sample list" S12 originally scrolled and
> searched **is now S10's capture browser** (cards with peak thumbnails, bank filter). What
> pulled INTO S10: the browser layout itself, the peak thumbnails, and the **bank filter**
> (a bank_book tab, distinct from name search). What stays in S12 and applies **to S10's
> browser**: (a) **scroll** for a bank longer than the panel, and (b) **type-to-filter
> search** (a name-substring narrow over the same cards, composing with S10's bank filter —
> bank filter picks the bank, search narrows within it). The scroll/search geometry is pure,
> layered over the `capture_browser` module S10 builds. Net: S12 = scroll + search over the
> S10 browser + numeric entry + ADSR; the browser *card* work is S10's.
- [ ] Scrollable, searchable capture browser: a scroll offset (wheel + scrollbar drag) so a
bank longer than the panel is fully reachable; a **type-to-filter search** that narrows
the drawn cards to matching display names, **composing with S10's bank filter** (bank
filter selects the bank; search narrows within it). Scroll/search layout + hit-test is
pure geometry (visible-card window, scrollbar thumb rect, search-box rect), layered over
S10's `capture_browser` module; filter/scroll state is transient UI state.
- [ ] Direct numeric entry for zone low/high/root: a click-to-edit field over the strip
(LICE text-entry idiom or a SWELL edit control on the child HWND) so a precise note can be
typed, not only dragged. Commits via `commitMapAndReload` like every other edit.
@@ -564,13 +642,23 @@ is **ReaSampler 9000**. Framing + propagation surfaces:
class **display name** string (in the factory registration), the `IPlugView` editor title
band (currently "ReaSampler Instrument"), the S6 embed-strip label, and the Phase S docs.
**Do NOT change the VST3 class UID** — instances in already-saved projects key off it; a
UID change orphans every existing instance (fork below).
- [ ] **Fork S-NAME-1 (Daniel's call): the binary filename.** Renaming the built VST3
module (e.g. `reasampler_9000.vst3`) is a compat break — REAPER keys a saved project's
plugin reference partly by filename, so a rename can require users to re-select the plugin
on existing instances. Lean: **keep the current filename, change only display strings**
(zero compat weight, the name shows everywhere the user reads), unless Daniel wants the
on-disk name to match. Flag, do not decide.
UID change orphans every existing instance.
- [ ] **Rename the binary filename too (S-NAME-1 SETTLED, Daniel 2026-07-26):** rename the
built VST3 module (CMake `OUTPUT_NAME` / target artifact — e.g. `reasampler_9000.vst3`)
alongside the display strings, so the on-disk name matches the product name. Record the
full rename surface: **CMake output name** (the second VST3 target's artifact name), the
**factory vendor/name strings**, the **`IPlugView` editor title**, and the **S6 embed
label**. Do NOT touch the **VST3 class UID** (unchanged — the compat anchor).
- [ ] **Compat verification (must-DAW-verify before shipping the rename):** the working
assumption is that REAPER **rebinds a saved instance by its VST3 class UID, not by the
module filename**, so a filename rename with an unchanged UID keeps saved projects working
(existing instances still resolve). **This is not yet confirmed from source** — a web
check surfaced a JUCE/VST3-replace-VST2 case suggesting REAPER's binding is more nuanced
than "UID only" (it can involve an FXID match), so treat UID-rebind as **to-verify, not
asserted fact**. **DAW-verify:** save a project with a ReaSampler 9000 instance under the
old filename, rename the module, reopen — confirm the instance rebinds and restores its
state. If REAPER does key partly on filename, fall back to keeping the current filename
(display-strings-only) and record that as the shipped choice.
## S17 — drop-and-load: drag a capture onto a track's FX button → instantiate ReaSampler 9000 with the capture loaded
**Goal:** Turn a bank capture into a playable instrument in one gesture. Today a drag