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:
+92
-34
@@ -1692,10 +1692,10 @@ id→slot map) is settled at build; the *contract* below holds either way.
|
||||
trimmed for content-height purposes (the recommendation: trim trailing empties for scroll
|
||||
extent, keep interior gaps).
|
||||
- **Reorder:** the user drags a card to a target slot within its bank; the pure reorder mutator
|
||||
moves that sample's position to the target slot, gap-preserving. (Whether a drop onto an
|
||||
occupied slot displaces/swaps vs. inserts-and-shifts is settled at build — recommendation:
|
||||
**drop-into-empty-slot places there; drop-onto-occupied inserts before and shifts the tail**,
|
||||
matching common file-manager reorder.)
|
||||
moves that sample's position to the target slot, gap-preserving. **Drop-into-empty-slot places
|
||||
there; drop-onto-occupied inserts-before and shifts the tail** (matching common file-manager
|
||||
reorder) — SETTLED as the *default* drop. (The **Alt+drop-onto-occupied = REPLACE** override is
|
||||
specified below under Drag disambiguation, F3.)
|
||||
|
||||
**JSON round-trip + migration (load-bearing).** `serialize`/`deserialize` stay lossless
|
||||
including positions (`deserialize(serialize(x)) == x`). **A pre-L7 project blob has no position
|
||||
@@ -1708,13 +1708,20 @@ round-trip + legacy migration.
|
||||
**Undo.** A reorder is **one Ctrl-Z** — the actions/shell layer wraps the mutation in a batched
|
||||
undo point (`Undo_BeginBlock2`/`EndBlock2`), matching every existing bank-verb's undo discipline.
|
||||
|
||||
**M9 overlap (flagged — Daniel's awareness).** M9 "slots" (capture-to-slot-N / insert-slot-N,
|
||||
MIDI-bindable, MPC-style) is **explicitly deferred (Daniel, 2026-07-26).** L7's sparse-slot
|
||||
model is a **partial overlap:** it builds the *addressable-position substrate* M9 would sit on,
|
||||
but L7 adds **no** slot-numbered capture/insert actions and **no** MIDI bindings. Building L7
|
||||
un-defers the *coordinate model* portion of M9, not the *action* portion. If Daniel wants the
|
||||
substrate to be explicitly M9-shaped (fixed numbered slots vs. a plain gap-preserving ordinal),
|
||||
that is a fork to settle before the model is built.
|
||||
**Position-model shape (F2 — SETTLED 2026-07-27: interchangeable substrate, NOT fixed slots).**
|
||||
The carrier is the **plain gap-preserving interchangeable-slot substrate** — a per-bank id→slot
|
||||
map (Daniel: *"I don't think I want fixed slots MPC style, but the substrate of interchangeable
|
||||
slots is valuable"*). It is **not** M9-shaped: no slot *identities*, no numbered/addressable
|
||||
slots that persist independent of their occupant, no slot actions, no MIDI-bindable slot numbers,
|
||||
no capture-to-slot-N. A slot is just a display position a sample occupies; dragging cards
|
||||
rearranges which sample sits where.
|
||||
|
||||
**M9 overlap (awareness note — unchanged intent).** M9 "slots" (capture-to-slot-N /
|
||||
insert-slot-N, MIDI-bindable, MPC-style) remains **explicitly deferred (Daniel, 2026-07-26).**
|
||||
The interchangeable substrate L7 builds still *eases* a future M9 revival — it lays the
|
||||
addressable-position groundwork M9 would sit on — but L7 adds **no** slot-numbered capture/insert
|
||||
actions and **no** MIDI bindings. The "plain vs. M9-shaped" sub-fork is closed: plain
|
||||
gap-preserving substrate, per F2 above.
|
||||
|
||||
### 2. Decorative metadata overlay (bars.beats · s.ms)
|
||||
|
||||
@@ -1728,17 +1735,17 @@ subtle shadowed variant for legibility over the peaks), subordinate to the wavef
|
||||
the speed constraint (no animation).** Pure formatting helpers (below) are unit-tested; only the
|
||||
kit draw is shell.
|
||||
|
||||
**Bars.beats source (FORK F1 — Daniel to confirm).** bars.beats.subdivisions requires a
|
||||
**tempo + time-signature** reference. `Sample` already carries `captureTempo` (BPM at capture)
|
||||
and `lengthBeats`, but **no time-signature.** Two options:
|
||||
- **(recommended) Capture-time stamp:** add `captureTimeSigNum` / `captureTimeSigDenom` to
|
||||
`Sample` + its JSON round-trip, stamped on the capture path (read the project meter at capture
|
||||
via the REAPER meter API — verify `TimeMap_GetTimeSigAtTime` / equivalent against the SDK at
|
||||
build). *Why recommended:* the label is **stable** as the project's tempo/meter later changes;
|
||||
a bank sample outlives the project state it was captured under, matching the existing
|
||||
`captureTempo` stamp philosophy. **Cost:** this is a **capture-path write beyond draw work** —
|
||||
its own flagged checkbox; old samples with no stamp fall back gracefully (blank musical
|
||||
read-out, or a documented assumed 4/4).
|
||||
**Bars.beats source (F1 — SETTLED 2026-07-27: capture-time stamp).** bars.beats.subdivisions
|
||||
requires a **tempo + time-signature** reference. `Sample` already carries `captureTempo` (BPM at
|
||||
capture) and `lengthBeats`, but **no time-signature.** **Decision: capture-time stamp** — add
|
||||
`captureTimeSigNum` / `captureTimeSigDenom` to `Sample` + its JSON round-trip, stamped on the
|
||||
capture path (read the project meter at capture via the REAPER meter API — verify
|
||||
`TimeMap_GetTimeSigAtTime` / equivalent against the SDK at build). bars.beats.subdivisions renders
|
||||
from the stamped tempo + meter, **stable under later project tempo/meter changes** — a bank sample
|
||||
outlives the project state it was captured under, matching the existing `captureTempo` stamp
|
||||
philosophy. This is a **capture-path write beyond draw work** (its own checkbox in the PLAN, now
|
||||
settled). Old samples with no stamp fall back gracefully (blank musical read-out, or a documented
|
||||
assumed 4/4).
|
||||
- **(rejected) Live project meter at draw time:** the label would drift under the card as the
|
||||
project tempo/meter changes, and would be wrong for any sample captured under a different
|
||||
meter than the project's current one.
|
||||
@@ -1766,30 +1773,81 @@ grid-card interaction states stay **visually distinct and coherent:**
|
||||
`accent/hot` outline on the target slot) must not be confusable with the purple selection
|
||||
border; spec the exact treatment at build.
|
||||
|
||||
### Drag disambiguation (FORK F3 — proposed rule, Daniel to confirm)
|
||||
### Drag disambiguation (F3 — SETTLED 2026-07-27)
|
||||
|
||||
In-grid reorder must coexist with the existing internal bank-move/copy drag and OS drag-out.
|
||||
**Proposed precedence (one clean rule, evaluated live during the drag):**
|
||||
**Precedence (one clean rule, evaluated live during the drag):**
|
||||
1. **Pointer leaves the client rect → OS drag-out** (unchanged; `drag_out::decideGesture` wins
|
||||
first — the existing invariant #4 boundary).
|
||||
2. **Else drop lands on a tab / the OTHER region's bank → move/copy** (unchanged; Ctrl = copy).
|
||||
3. **Else drop lands within the SAME bank's own grid → reorder-to-slot** (new).
|
||||
- Onto an **empty** slot → place there.
|
||||
- Onto an **occupied** slot, **no modifier** → insert-before-and-shift-tail (the default,
|
||||
above).
|
||||
- Onto an **occupied** slot, **Alt held** → **REPLACE** the occupant (below).
|
||||
|
||||
So: leave-client wins → else other-bank wins → else same-bank-grid = reorder. The precedence is
|
||||
encoded in a **pure decision helper** (mirror `drag_out::decideGesture`); the shell reads the
|
||||
live pointer + focused region + client rect and calls it. This keeps the reorder gesture from
|
||||
ever stealing an intended bank-move or OS-drag, and keeps a same-bank in-grid drag from being
|
||||
mis-read as a no-op (today a same-bank drop is a no-op; L7 gives it reorder meaning).
|
||||
live pointer + focused region + client rect + **modifier state (Alt)** and calls it. This keeps
|
||||
the reorder gesture from ever stealing an intended bank-move or OS-drag, and keeps a same-bank
|
||||
in-grid drag from being mis-read as a no-op (today a same-bank drop is a no-op; L7 gives it
|
||||
reorder meaning).
|
||||
|
||||
**Alt+drop-onto-occupied = REPLACE (SETTLED 2026-07-27).** Holding **Alt** at drop over an
|
||||
occupied slot **replaces the occupant** instead of inserting-and-shifting. Replace semantics,
|
||||
precisely:
|
||||
- The replaced sample is **removed from THAT bank's index only** — same semantics as the existing
|
||||
remove-from-bank verb (`BankBook::removeSample` index-only). **The file stays on disk;** the
|
||||
owned-manifest and Phase R prune govern its bytes. If the replaced sample's last reference
|
||||
disappears, that is exactly the existing cross-bank-reference story (`hashReferencedElsewhere`
|
||||
reports whether the hash is still referenced elsewhere; prune later reclaims a now-orphaned
|
||||
owned file). Replace introduces **no new deletion authority** — it never touches the disk.
|
||||
- The dragged sample then takes the vacated slot (the slot's position is preserved; only its
|
||||
occupant changes).
|
||||
- **Pool case (un-evacuable pool — SETTLED rule).** The pool's privileges (un-deletable,
|
||||
un-renamable, **un-evacuable**, never zero banks) are enforced in `bank_book`. **Alt+Replace is
|
||||
allowed in the pool only when it does not violate a pool privilege.** Concretely: replacing a
|
||||
pool entry is an index-only removal of that pool sample; it is **permitted** as long as it does
|
||||
not empty the pool below the pool's floor and does not remove the *last* reference in a way the
|
||||
pool's rules forbid. The consistent rule the model enforces: **the replace's index-removal step
|
||||
is the same operation as remove-from-bank, and it must pass the same pool-privilege guard that
|
||||
remove already applies — if remove-from-pool would be rejected for that sample, Alt+Replace over
|
||||
it is rejected too** (the drop falls back to a no-op or the default insert-shift; settle the
|
||||
exact rejection UX at build). No special pool-only replace path; one rule, guarded by the
|
||||
existing pool invariants.
|
||||
|
||||
**Drop-result cursor cues (SETTLED 2026-07-27 — REAPER-idiomatic special cursors).** During a
|
||||
drag the cursor must indicate **what the drop will do**, following REAPER's idiomatic use of
|
||||
distinct action cursors. The cue set:
|
||||
- **Reorder-to-slot** (within the same bank's grid) — a move/reorder cursor.
|
||||
- **Move/copy to another bank or tab** — the move (or copy, when Ctrl is held) cursor, matching
|
||||
the existing internal-drag semantics.
|
||||
- **OS drag-out** (pointer left the client rect) — the OS copy/drag cursor (owned by the OS drag
|
||||
loop once handed off).
|
||||
- **Replace** — a distinct replace cursor, shown **only when Alt is actually held over an
|
||||
occupied slot** (i.e. only when precedence resolves to the Alt+replace case). It must not appear
|
||||
over an empty slot or when Alt is not held.
|
||||
|
||||
**Shell mechanism:** the shell sets the cursor via Win32/SWELL `SetCursor` (REAPER ships its own
|
||||
action-cursor idiom as the visual reference — match its feel; load or synthesize kit-consistent
|
||||
cursors at build). **The *decision* of which cue applies stays in the pure gesture/disambiguation
|
||||
helper** — the same helper that resolves precedence returns the resolved gesture (reorder / move /
|
||||
copy / os-drag-out / replace), and the shell maps that pure result to a cursor. No cue logic in
|
||||
the shell; the shell only owns the `SetCursor` call and the cursor resources.
|
||||
|
||||
### Pure/shell discipline (L7)
|
||||
|
||||
Model: the position carrier + gap semantics + reorder mutator + JSON round-trip/migration are
|
||||
**pure** (in `bank_book`, CTest-covered to the bar of its existing round-trip). Layout: the
|
||||
sparse-aware slot↔rect math + point→slot hit-test + the drag-disambiguation decision are **pure**
|
||||
(extend `bank_grid`; mirror `mode_switch` / `drag_out::decideGesture`). Formatting: the
|
||||
bars.beats and s.ms formatters are **pure**. Shell (DAW-bound): the reorder drag wiring +
|
||||
drop-target highlight, the capture-time-signature stamp (F1) read on the capture path, and the
|
||||
kit overlay/selection-border draw. The L1 kit draws; no palette/font decision re-opened.
|
||||
Model: the position carrier + gap semantics + **reorder mutator + Alt-replace mutator** (the
|
||||
latter reusing the existing index-only remove-from-bank semantics + pool-privilege guard) + JSON
|
||||
round-trip/migration are **pure** (in `bank_book`, CTest-covered to the bar of its existing
|
||||
round-trip). Layout: the sparse-aware slot↔rect math + point→slot hit-test + the
|
||||
drag-disambiguation decision (**including the resolved-gesture result that drives the cursor cue,
|
||||
and the Alt-over-occupied → replace resolution**) are **pure** (extend `bank_grid`; mirror
|
||||
`mode_switch` / `drag_out::decideGesture`). Formatting: the bars.beats and s.ms formatters are
|
||||
**pure**. Shell (DAW-bound): the reorder/replace drag wiring + drop-target highlight, the
|
||||
**cursor `SetCursor` call mapping the pure resolved-gesture to a cursor resource**, the
|
||||
capture-time-signature stamp (F1, settled) read on the capture path, and the kit
|
||||
overlay/selection-border draw. The L1 kit draws; no palette/font decision re-opened.
|
||||
|
||||
## The L3 gate + Phase S coordination contract
|
||||
|
||||
|
||||
Reference in New Issue
Block a user