S17 drop-and-load onto track FX button; S13 editor drop-accept (relay degraded)

S17: drag_out InstrumentDrop gesture + instrument_drop blob reusing the
instrument's own serializer; bank_panel FX hover-track + add-VST/vst_chunk
inject. S13 relay deferred (read-only bridge) — editor shows drop affordance.
This commit is contained in:
2026-07-27 03:52:26 -04:00
parent 06494c654e
commit 26e2bf2fc6
13 changed files with 779 additions and 54 deletions
+62 -11
View File
@@ -634,17 +634,31 @@ a clear affordance pointing there.
without it via a direct reload). **Spike — do not promise the drop-onto-editor path until
the relay is proven.**
- [ ] Editor-window drop target: accept `WM_DROPFILES`/`IDropTarget` on the editor child
- [x] Editor-window drop target: accept `WM_DROPFILES`/`IDropTarget` on the editor child
HWND (the same SWELL/Win32 surface `bank_panel` owns), extracting the dropped file
path(s). Windows-only (D5). This is the *acceptance* half; the ingest is the extension's.
**Landed:** the editor child window calls `DragAcceptFiles(TRUE)` on attach and handles
`WM_DROPFILES` (`reasampler_editor.cpp`). Windows-only (D5).
- [ ] Cross-artifact ingest relay (the S8-flagged spike): the editor hands the dropped
path + this instance's identity to the extension as a **bank-ingest request** over the
agreed seam (the instrument never writes the bank). **Prove the relay mechanism before
promising it**; if gnarly, fall back to the S8 docked-panel drop path and mark
drop-onto-editor deferred.
- [ ] UX degrade path: when the relay is unavailable/unproven, the editor shows a clear
drop-onto-editor deferred. **SPIKE VERDICT (ps-w12, 2026-07-27): DEGRADED — relay
deferred.** The instrument's REAPER bridge (`reaper_bridge`) is deliberately READ-ONLY
(resolves only `GetProjExtState`, never `SetProjExtState`); a relay would need (a) a new
instrument WRITE seam into ext-state and (b) an extension-side timer poller servicing a
drop-ingest inbox key with a claim/clear nonce — the SAME cross-process handshake race the
S17 spec rejected for its own alternative (A). Both the read-only-instrument boundary and
the new poller are load-bearing design calls, so the relay is deferred to a future wave and
surfaced as a decision, not crossed unilaterally. The shipped ingest gesture stays
drop-onto-docked-panel (S8). The drop-onto-editor path degrades cleanly (below).
- [x] UX degrade path: when the relay is unavailable/unproven, the editor shows a clear
"drop files on the ReaSampler panel to add" affordance rather than silently swallowing the
drop — the shipped ingest gesture stays discoverable either way.
drop — the shipped ingest gesture stays discoverable either way. **Landed:** the editor
ACCEPTS the drop and flashes a transient banner ("drop files onto the ReaSampler bank panel
to add them") that decays over a few sync ticks, plus a persistent affordance line in the
empty state ("drop a file onto the ReaSampler bank panel"). No file is ingested; NO timeline
item is ever inserted (the hard invariant — the editor only displays guidance).
## S15 — sampling modes: Trigger vs Gate (per-sample play-mode; core + editor)
**Goal:** Give each played sample a **play mode****Gate** (classic held note) or
@@ -1020,22 +1034,41 @@ the editor). S17 is the third integration gesture: drop *onto a track's FX butto
instantiate a player. Gated on the rest of Phase S; the instrument seam is a Phase S
artifact, not extension-only.
- [ ] Extend the `drag_out` pure module with the third gesture: `decideGesture` (or a
- [x] Extend the `drag_out` pure module with the third gesture: `decideGesture` (or a
successor) returns `InstrumentDrop` when a drag armed with a single capture is over
REAPER's UI outside the panel client rect, `OsDrag` only when it has left REAPER
entirely, `Internal`/`OsDrag`/`None` otherwise unchanged. Pure over (drag state +
pointer + panel rect + an "over-own-UI" predicate the shell supplies). Unit-tested —
the existing `drag_out` invariants (M11) must not regress.
- [ ] Shell (extension): hover-track the pointer over REAPER's UI during the drag,
the existing `drag_out` invariants (M11) must not regress. **Landed:** `DragGesture::
InstrumentDrop` + two defaulted `DragState` fields (`singleCapture`, `overReaperUi`, the
shell-supplied predicate). Defaults false, so an M11 caller filling only
`{dragging, hasArmedSamples}` gets byte-identical M11 behavior — the existing tests are
the non-regression proof. **OPEN QUESTION RESOLVED (multi-capture over FX button):
REJECT** — only `singleCapture` arms InstrumentDrop; a multi payload over REAPER's UI
falls through to `OsDrag` (the natural multi-file drag-out), matching the spec's Tier-0
reject-or-first lean toward reject.
- [x] Shell (extension): hover-track the pointer over REAPER's UI during the drag,
resolve the hovered track + its FX button (verify the TCP/FX-button hit surface against
the SDK — see must-verify), highlight it as a drop target, and on release drive the
drop. Extends the `bank_panel` drag hook alongside the M11 `drag_out_win` path.
- [ ] Shell (extension): on drop, `TrackFX_AddByName(track, "VST3:ReaSampler 9000",
**Landed:** `instrument_drop_win::resolveFxDropTarget` wraps `GetThingFromPoint`
(verified present; its info string reports `"fx_chain"`/`"fx_N"` for the FX region and a
null-track-empty-info for off-REAPER). **OPEN QUESTION RESOLVED (FX hotspot vs. whole
TCP): FX HOTSPOT** — the drop target is the FX region specifically, decided from the SDK's
own hit-test string (info prefix `"fx_"`), not a home-grown geometry guess. The
bank_panel drag hook now hover-tracks outside the client rect (holding internal-drag
capture, NOT the modal OS loop) and only hands to `drag_out_win` (OsDrag) when the pointer
has left REAPER entirely.
- [x] Shell (extension): on drop, `TrackFX_AddByName(track, "VST3:ReaSampler 9000",
false, /*instantiate*/ negative)` to always add a fresh instance; capture the returned
FX index; then invoke the load-capture seam to point the new instance at the dragged
capture. Batched into one REAPER undo point (`Undo_BeginBlock2`/`EndBlock2`) so the
whole gesture is one Ctrl-Z (mirrors the bank-verb undo discipline).
- [ ] **ReaSampler 9000 (Phase S artifact):** add the **load-capture seam** — the entry
whole gesture is one Ctrl-Z (mirrors the bank-verb undo discipline). **Landed:**
`instrument_drop_win::performInstrumentDrop` — `TrackFX_AddByName` with the CHANNEL-correct
name (`"VST3:" + app_version::vstPluginName()`, so a beta extension drops the beta VST),
negative instantiate, then `TrackFX_SetNamedConfigParm(..., "vst_chunk", blob)`, all in one
`Undo_BeginBlock2`/`EndBlock2` block. NEVER inserts a timeline item.
- [x] **ReaSampler 9000 (Phase S artifact):** add the **load-capture seam** — the entry
point that lets the just-added instance be told which capture to play via **(B)
component-state injection**: the instrument's `getChunk`/`setChunk` must round-trip a
component-state blob whose byte layout is the **shared cross-artifact contract** (defined
@@ -1043,9 +1076,27 @@ artifact, not extension-only.
via `TrackFX_SetNamedConfigParm(..., "vst_chunk", ...)`. This is the cross-artifact half;
it lands in the instrument, not the extension. **Coordination dependency:** the blob
format must be agreed between extension and instrument before either half is final.
- [ ] Tests: gesture disambiguation (inside-panel / over-REAPER-UI / left-REAPER) across
**Landed (no NEW instrument code needed — the seam already exists):** the instrument's
`setState`/`getState` already round-trip the full `ComponentState` via
`sample_map::serializeComponentState`/`deserializeComponentState` (S10). The extension side
REUSES that exact serializer through the new pure `instrument_drop` module
(`buildInstrumentDropChunk` -> `serializeComponentState` -> base64) — the shared-writer
requirement is met STRUCTURALLY (one serializer, called from both artifacts), so the blob
format cannot drift. Pure round-trip test decodes back through the instrument's OWN reader
(`deserializeComponentState`) and asserts the capture is selected. **DAW-VERIFY (the one
live unknown):** whether REAPER's `vst_chunk` write-parm expects the plugin's raw
IComponent-state bytes as we write them, or wraps them in a REAPER container header — the
load-bearing caveat is only confirmable in the DAW; if REAPER wraps, the fix is to match
its container, the shared serializer stays.
- [x] Tests: gesture disambiguation (inside-panel / over-REAPER-UI / left-REAPER) across
single- and multi-capture payloads; FX-button hit resolution (pure geometry where it
can be factored out); M11 OS drag-out and internal bank-to-bank drag both unchanged.
**Landed:** `test_drag_out.cpp` adds the InstrumentDrop cases (single over REAPER UI ->
InstrumentDrop; single inside -> Internal; single off-REAPER -> OsDrag; multi over UI ->
OsDrag; not-dragging short-circuits) with the M11 cases retained as the non-regression
guard; `test_instrument_drop.cpp` is the blob round-trip + base64 codec coverage. FX hit
resolution is REAPER-API-bound (`GetThingFromPoint`), so it is DAW-verified in the shell,
not pure-tested — noted honestly.
## S18 — VST3 channel isolation: a beta ReaSampler 9000 that pairs with the beta extension only
**Goal:** Extend Phase V's beta/stable channel split (V4 — the *extension* is fully