docs(product): settle Phase L §L7 forks F1/F2/F3 (capture-time stamp, interchangeable substrate, drag rule + cursor cues + Alt-replace)

This commit is contained in:
2026-07-27 01:19:36 -04:00
parent e4bcc8f075
commit 7dc77e51b5
2 changed files with 174 additions and 88 deletions
+82 -54
View File
@@ -389,50 +389,55 @@ is model + layout + draw only; the "capture ≠ placement" principle is untouche
> per-sample ordinal/slot to the persisted state and its JSON round-trip + a migration for
> existing projects — scoped honestly below, model work separated from draw work.
> **Forks for Daniel (surface before build — see CONTEXT.md §L7):**
> - **F1 — bars.beats source.** `Sample` already carries `captureTempo` (BPM at capture) and
> `lengthBeats`, but **no time-signature field.** bars.beats.subdivisions needs a
> time-signature reference. **Recommendation: a capture-time stamp** (add `captureTimeSigNum`
> /`captureTimeSigDenom` to `Sample`, stamped on the capture path) so the label is stable as
> the project tempo/meter later changes — matching the existing `captureTempo` stamp
> philosophy. This adds a **capture-path stamp beyond draw work** (its own checkbox below,
> flagged). Alternative (live project meter at draw time) is rejected: the label would drift
> under the card as the project changes, and a bank sample outlives the project state it was
> captured under.
> - **F2 — M9 un-deferral awareness.** M9 "slots" (capture-to-slot-N / insert-slot-N,
> MIDI-bindable, MPC-style) is **explicitly deferred (Daniel, 2026-07-26).** L7's sparse
> placement is a **partial overlap** — both need addressable grid positions. Building L7's
> sparse-slot model effectively lays the *position substrate* M9 would sit on, but L7 does
> **not** add slot-numbered capture/insert actions or MIDI bindings. Flagged so Daniel knows
> L7 un-defers the *coordinate model* portion of M9, not the *action* portion. If Daniel wants
> the slot substrate to be explicitly M9-shaped (numbered addressable slots vs. a plain
> gap-preserving ordinal), that is a fork to settle before the model checkbox.
> - **F3 — drag-disambiguation rule (proposed, Daniel to confirm).** The panel already has
> internal bank-move/copy drag and OS drag-out. In-grid reorder must coexist. **Proposed
> rule:** a drag whose drop lands **within the SAME bank's own grid** = **reorder-to-slot**;
> a drop on **a tab / the other region's bank** = **move/copy** (as today); a drag that
> **leaves the client rect** = **OS drag-out** (as today). One clean precedence: leave-client
> wins → else other-bank wins → else same-bank-grid = reorder. See CONTEXT.md §L7.
> **Forks — all SETTLED 2026-07-27 (see CONTEXT.md §L7 for full detail):**
> - **F1 — SETTLED: capture-time stamp.** Add `captureTimeSigNum` / `captureTimeSigDenom` to
> `Sample`, stamped on the capture path alongside the existing `captureTempo`;
> bars.beats.subdivisions renders from the stamped tempo + meter, stable under later project
> changes. Live-meter-at-draw-time rejected (would drift). The capture-path stamp checkbox
> below is **settled, not flagged.**
> - **F2 — SETTLED: plain interchangeable-slot substrate, NOT fixed MPC-style slots.** The
> carrier is a **per-bank id→slot map** (gap-preserving interchangeable slots): a slot is just a
> display position an occupant sits in — **no slot identities, no numbered/addressable slots, no
> slot actions, no MIDI-bindable slot numbers, no capture-to-slot-N.** The "plain vs. M9-shaped"
> sub-fork is closed (plain). M9 (capture-to-slot-N / insert-slot-N, MIDI-bindable, MPC-style)
> stays **explicitly deferred (Daniel, 2026-07-26);** the substrate still eases a future M9
> revival but L7 adds no M9 actions or bindings.
> - **F3 — SETTLED + EXTENDED.** Drag precedence: leave-client → OS drag-out; else tab/other-bank
> → move/copy (Ctrl = copy); else same-bank grid → reorder-to-slot. **Extensions:**
> (a) **drop-result cursor cues** — the cursor indicates the drop result via REAPER-idiomatic
> special cursors (reorder / move / copy / OS-drag-out / replace); the shell sets it via
> Win32/SWELL `SetCursor`, but the *decision* of which cue applies stays in the pure gesture
> helper. (b) **Alt+drop-onto-occupied = REPLACE** — default occupied-slot drop stays
> insert-and-shift; holding Alt replaces the occupant (index-only removal, existing
> remove-from-bank semantics — file stays on disk, prune governs bytes, `hashReferencedElsewhere`
> handles last-reference). **Pool rule:** Alt+Replace in the un-evacuable pool is allowed only
> when the same pool-privilege guard that `remove` applies would permit removing that sample —
> one rule, no pool-only path; the replace cursor cue shows only when Alt is actually held over
> an occupied slot.
**Verify (in DAW):** capture several samples; drag a card to a new position within its bank —
the order persists across project save/reload and Ctrl-Z restores the prior order; drag the
first card below the second so the first row is empty — the gap holds across reload; each card
shows bars.beats.subdivisions bottom-left and s.ms bottom-right, legible over the peaks; a
selected card shows the normal cell with a purple (`accent/tertiary`) border — no inversion —
and hover / focus / drag-target remain distinct; dragging a card to another bank's tab still
moves/copies (not reorder), and dragging out of the panel still starts an OS file drag; an
existing project (saved before L7) loads cleanly with cards in their prior insertion order.
and hover / focus / drag-target remain distinct; during any drag the cursor shows the correct
special cue (reorder within the bank, move/copy over a tab/other bank, OS drag-out past the client
edge); Alt+drop over an occupied card **replaces** the occupant (the replaced sample leaves that
bank's grid but its file survives on disk; if it was the last reference, prune later reclaims it),
and the replace cursor appears only while Alt is held over an occupied slot; dragging a card to
another bank's tab still moves/copies (not reorder), and dragging out of the panel still starts an
OS file drag; an existing project (saved before L7) loads cleanly with cards in their prior
insertion order.
**Depends on:** L1 (the kit) for all draw; sequences **after L6** (both rework the same
`bank_panel` grid). **Ungated by Phase S** (the dock panel is on dev); independent of the L3
gate. No `capture` / `insert` behavior change; no palette/font decision re-opened.
**(a) Persisted order + sparse-position model (pure, hard-tested) — model work, separate from draw:**
- [ ] Add an explicit per-sample **display position** to the persisted bank state (gap-preserving
ordinal or slot coordinate — settle the shape against F2). Decide the home: `bank_model`
is stated untouched by `bank_book`'s wrapping — position is a **per-bank display concern**, so
it belongs with the bank's membership, not on `Sample`. Candidate: a per-`Bank` ordered/keyed
position map in `bank_book` (id → slot), leaving `bank_model` / `Sample` untouched. Settle the
exact carrier at build.
- [ ] Add an explicit per-sample **display position** to the persisted bank state — a
**per-`Bank` id→slot map in `bank_book`** (gap-preserving interchangeable slots, per F2
settled). `bank_model` / `Sample` stay untouched (position is a per-bank display concern, not a
`Sample` property — a copy of a sample into two banks may sit at different slots). **No slot
identities / numbered addressable slots** — a slot is just a position an occupant sits in.
- [ ] JSON round-trip for the position data (`serialize`/`deserialize` remain lossless:
`deserialize(serialize(x)) == x` including positions).
- [ ] **Migration for existing projects** (old blobs with no position data): load cleanly with a
@@ -440,32 +445,54 @@ gate. No `capture` / `insert` behavior change; no palette/font decision re-opene
a pre-L7 project is visually identical on first load post-L7.
- [ ] **Gap semantics on insert / delete / prune** (pure, tested): a new capture takes the next
free slot after the last occupied one (append); a delete/remove/prune **leaves a gap** (does
not re-pack) so positions stay stable; define whether an empty trailing tail is trimmed.
not re-pack) so positions stay stable. **Confirmed defaults:** trailing empty tail trimmed for
scroll-extent purposes; interior gaps preserved.
- [ ] **Reorder mutator** (pure): move sample `id` to target slot within its bank, gap-preserving;
deterministic; unit-tested.
- [ ] Undo integration: a reorder is one Ctrl-Z (batched undo point in the actions/shell layer,
matching the existing bank-verb undo discipline).
deterministic; unit-tested. Drop-into-empty places there; **drop-onto-occupied (default, no
modifier) inserts-before-and-shifts-tail.**
- [ ] **Alt-replace mutator** (pure): Alt+drop over an occupied slot **replaces the occupant**
index-only removal of the occupant from THAT bank (reuse the existing remove-from-bank
semantics: file stays on disk, owned-manifest/prune govern bytes, `hashReferencedElsewhere`
handles the last-reference case), then the dragged sample takes the vacated slot (position
preserved). **Pool guard:** if the sample sits in the un-evacuable pool, the replace's
index-removal step passes the **same pool-privilege guard that `remove` applies** — if
remove-from-pool would be rejected for that sample, Alt+Replace over it is rejected (falls back
to no-op / default insert-shift; settle rejection UX at build). One rule, no pool-only path.
Deterministic; unit-tested. **No new deletion authority — never touches disk.**
- [ ] Undo integration: a reorder **or a replace** is one Ctrl-Z (batched undo point in the
actions/shell layer, matching the existing bank-verb undo discipline).
**(b) Reorder-drag gesture + slot hit-test (pure geometry + shell wiring):**
- [ ] **Sparse-aware grid layout** (pure, extend `bank_grid`): map per-slot positions (with gaps)
to `CellRect`s — an empty slot is drawn empty (or skipped), not back-filled. Slot ↔ pixel-rect
math + point→slot hit-test stay pure and unit-tested (mirror `mode_switch`/`bank_grid`).
- [ ] **Drag-disambiguation** (F3): drop within the same bank's grid = reorder-to-slot; drop on a
tab / other bank = move/copy (unchanged); leave client = OS drag-out (unchanged). Precedence
- [ ] **Drag-disambiguation** (F3): drop within the same bank's grid = reorder-to-slot (empty =
place; occupied+no-modifier = insert-shift; **occupied+Alt = replace**); drop on a tab / other
bank = move/copy (unchanged, Ctrl = copy); leave client = OS drag-out (unchanged). Precedence
encoded in a **pure decision helper** (mirror `drag_out::decideGesture`); shell reads live
pointer + region + client rect and calls it.
pointer + region + client rect + **modifier state (Alt)** and calls it. The helper returns the
**resolved gesture** (reorder / move / copy / os-drag-out / replace) so the shell can map it to
a cursor cue.
- [ ] **Drop-result cursor cues** (F3 extension): the shell sets a REAPER-idiomatic special cursor
per the pure resolved-gesture (reorder / move / copy / OS-drag-out / replace) via Win32/SWELL
`SetCursor`; the **replace** cursor shows only when Alt is actually held over an occupied slot.
**Cue-decision logic stays in the pure helper;** the shell owns only the `SetCursor` call and
the cursor resources (load/synthesize kit-consistent cursors at build; REAPER's own action
cursors as the visual reference).
- [ ] Shell wiring: on a same-bank reorder drop, resolve the target slot via the pure hit-test and
call the reorder mutator; drop-target slot highlight during the drag (distinct from the existing
region/tab drop highlights).
call the reorder mutator (or, when Alt is held over an occupied slot, the **Alt-replace
mutator**); drop-target slot highlight during the drag (distinct from the existing region/tab
drop highlights, and — for a pending replace — distinct from the pending-reorder highlight).
**(c) Metadata overlay (pure formatting + kit draw):**
- [ ] **Pure formatters, unit-tested:** `bars.beats.subdivisions` from `lengthSeconds` +
`captureTempo` + capture-time signature (F1) and `seconds.milliseconds` from `lengthSeconds`.
Deterministic string output; edge cases (zero length, missing tempo → graceful blank/fallback).
- [ ] **[FLAGGED — F1, capture-path stamp beyond draw work] Capture-time signature stamp:** add
- [ ] **Capture-time signature stamp (F1, settled — capture-path write beyond draw work):** add
`captureTimeSigNum` / `captureTimeSigDenom` to `Sample` + its JSON round-trip, stamped on the
capture path (read the project meter at capture). Only needed if F1 resolves to the capture-time
stamp (recommended). Old samples with no stamp fall back gracefully in the formatter.
capture path (read the project meter at capture — verify `TimeMap_GetTimeSigAtTime` / equivalent
against the SDK at build). Old samples with no stamp fall back gracefully in the formatter
(blank musical read-out, or a documented assumed 4/4).
- [ ] **Kit draw:** overlay the two strings on the card — bars.beats bottom-LEFT, s.ms
bottom-RIGHT — via the kit `text()` in the micro / value-mono class, `text/dim` (or a subtle
shadowed variant for legibility over the waveform); **decorative, non-interactive** (no
@@ -513,15 +540,16 @@ Phase S to reach dev. Phase S feature work runs in parallel, ungated by Phase L.
pure module; sourced from the registered action phrase, prefix stripped at draw time); item-move
and track-tag action ids confirmed; active-mode read confirmed via the same
`view().activeModeId()` the footer toggle uses.
- **L7 model + tempo surface (L7)** — settle at L7 build/spec-review: (1) **position carrier**
where the per-sample display position lives (candidate: a per-`Bank` id→slot map in `bank_book`,
keeping `bank_model`/`Sample` untouched per CLAUDE.md); (2) **F1 bars.beats source** — confirm
the capture-time signature stamp with Daniel (recommended) vs. live project meter; the stamp adds
a capture-path write (its own flagged checkbox); (3) **F2 M9 overlap** — confirm L7's sparse-slot
substrate is not mis-scoped as M9's slot *actions*; (4) **F3 drag rule** — confirm the
same-bank-grid = reorder / other-bank = move-copy / leave-client = OS-drag precedence. Verify the
REAPER meter-read API for the stamp (`TimeMap_GetTimeSigAtTime` or equivalent) against the SDK at
build.
- **L7 model + tempo surface (L7)** — forks F1/F2/F3 **all settled 2026-07-27** (see the §L7
forks block above and CONTEXT.md §L7): position carrier = per-`Bank` id→slot map in `bank_book`
(plain interchangeable substrate, `bank_model`/`Sample` untouched); F1 = capture-time signature
stamp; F3 = drag precedence + REAPER-idiomatic cursor cues + Alt-replace-over-occupied with the
stated pool guard. **Remaining build-time verification (SDK/API, not product decisions):**
(1) the REAPER meter-read API for the F1 stamp (`TimeMap_GetTimeSigAtTime` or equivalent) —
verify signature/argument order against the SDK; (2) the Win32/SWELL `SetCursor` path and cursor
resource loading/synthesis for the drop-result cues — confirm the cursor idiom against SWELL and
REAPER's own action cursors; (3) confirm the existing remove-from-bank pool-privilege guard is
reusable as-is for the Alt-replace index-removal step.
---