docs(product): spec Phase L L7 — capture ordering, card metadata, and selection styling
Persisted gap-preserving per-bank display order + drag-drop reorder (model change, JSON round-trip + migration), decorative bars.beats/s.ms overlay, tertiary-border selection restyle. Model work separated from draw work; forks flagged (meter source, M9 substrate overlap, drag disambiguation).
This commit is contained in:
+148
@@ -1643,6 +1643,154 @@ predicate). The `TrackPopupMenu` popup, the `Main_OnCommand` dispatch, and the t
|
||||
timer + LICE overlay draw are the only DAW-bound pieces; the L1 kit draws. **L5 resolves its
|
||||
menu/tooltip/action-id specifics against the landed L4 `bank_panel`** (build-time confirmation).
|
||||
|
||||
## L7 capture ordering · card metadata · selection styling (ordering model · overlay · tertiary-border selection)
|
||||
|
||||
L7 is the **next ungated dock-panel pass over the same `bank_panel` grid** L4–L6 built. Unlike
|
||||
L4/L5 (pure layout over existing actions), **L7 (1) is a persisted-model change** — it adds an
|
||||
explicit per-sample display order with gap-preserving sparse placement to the persisted bank
|
||||
state and its JSON round-trip. (2) and (3) are draw-only. **No new capture/placement behavior;**
|
||||
the "capture ≠ placement" principle is untouched. Drawn through the L1 kit in the DS-2
|
||||
grey-neutral + three-accent-pastel palette; **no palette/font decision is re-opened.** Ungated
|
||||
by Phase S; independent of the L3 gate. Sequences AFTER L6.
|
||||
|
||||
**Research baseline (dev, confirmed at spec time).** The grid draws in **`BankIndex` insertion
|
||||
order**: `BankIndex` holds `std::vector<Sample> samples_` in insertion order (`all()`), and
|
||||
`bank_grid::computeCellRects` tiles exactly one contiguous rect per sample left-to-right,
|
||||
top-to-bottom — **no explicit ordinal, no sparse slots today.** `Sample` already carries
|
||||
`lengthSeconds`, `lengthBeats`, and `captureTempo` (BPM at capture) but **no time-signature
|
||||
field.** The selected cell is drawn (`bank_panel::drawThumbnail`) as
|
||||
`InteractionState::Active` (accent-fill surface) + an `accent/primary` border + an *inverted*
|
||||
waveform (`bg/base`), with focus as a `text/primary` double-line ring. Drag today: press on a
|
||||
selected cell arms a drag; crossing a threshold begins it; leaving the client rect hands off to
|
||||
OS drag-out (`drag_out::decideGesture`); a drop on a tab / the other region moves (or copies on
|
||||
Ctrl) the samples to that bank. **There is no same-bank in-grid reorder today.**
|
||||
|
||||
### 1. Persisted deterministic order + sparse (gap-preserving) placement
|
||||
|
||||
Each bank (and the pool) carries an **explicit, persisted display position per sample** — the
|
||||
grid no longer derives order from insertion order. Positions are **gap-preserving**: a sample
|
||||
may occupy a slot that leaves earlier slots empty (an empty first row above an occupied second
|
||||
row is a valid, persisted state).
|
||||
|
||||
**Where the position data lives (the CLAUDE.md constraint).** `bank_model` / `Sample` are stated
|
||||
**untouched by `bank_book`'s wrapping** (no `bankId` on `Sample`; a bank is a logical grouping
|
||||
over the shared pool). Display position is a **per-bank display concern**, so it belongs with the
|
||||
bank's membership, **not on `Sample`** — a copy of a sample into two banks can sit at different
|
||||
slots. **Recommended carrier: a per-`Bank` ordered position map in `bank_book`** (sample id →
|
||||
slot), leaving `bank_model` untouched. The exact carrier (ordered id list with gaps vs. explicit
|
||||
id→slot map) is settled at build; the *contract* below holds either way.
|
||||
|
||||
**Ordering contract (deterministic, gap-preserving):**
|
||||
- **Deterministic:** the grid iterates positions in ascending slot order; ties are impossible
|
||||
(one sample per slot). The order is fully determined by the persisted position data, not by
|
||||
insertion order or hash.
|
||||
- **Insert (new capture):** a new capture takes the **next free slot after the last occupied
|
||||
slot** (append). It never fills an earlier gap automatically — a gap is a user's deliberate
|
||||
layout, not a hole to be plugged.
|
||||
- **Delete / remove / prune:** removing a sample **leaves its slot empty** (does not re-pack) so
|
||||
every other sample keeps its position. Define at build whether a purely-empty trailing tail is
|
||||
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.)
|
||||
|
||||
**JSON round-trip + migration (load-bearing).** `serialize`/`deserialize` stay lossless
|
||||
including positions (`deserialize(serialize(x)) == x`). **A pre-L7 project blob has no position
|
||||
data** → on load it defaults to **current insertion order, densely packed (no gaps)**, so a
|
||||
project saved before L7 is visually identical on first post-L7 load. This default is the
|
||||
migration; it is one-way (once re-saved, the position data is authoritative). Pure and
|
||||
hard-tested — this is a persisted-model change, tested to the same bar as `bank_book`'s existing
|
||||
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.
|
||||
|
||||
### 2. Decorative metadata overlay (bars.beats · s.ms)
|
||||
|
||||
Each card overlays, on the waveform, two decorative read-outs of **capture length**:
|
||||
- **bottom-LEFT:** length in **bars.beats.subdivisions** (musical).
|
||||
- **bottom-RIGHT:** length in **seconds.milliseconds** (wall-clock).
|
||||
|
||||
**Contract:** decorative and **non-interactive** — no hit-test, no hover, no selection role.
|
||||
Drawn via the L1 kit `text()` in the **micro / value-mono** type class, in `text/dim` (or a
|
||||
subtle shadowed variant for legibility over the peaks), subordinate to the waveform. **Respects
|
||||
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).
|
||||
- **(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.
|
||||
|
||||
**Formatting helpers (pure, tested).** `bars.beats.subdivisions` from `lengthSeconds` +
|
||||
`captureTempo` + capture-time signature; `seconds.milliseconds` from `lengthSeconds`.
|
||||
Deterministic; graceful on edge cases (zero length → both read empty/`0`; missing tempo → blank
|
||||
musical read-out, keep the s.ms read-out).
|
||||
|
||||
### 3. Selection styling — tertiary border replaces inversion
|
||||
|
||||
A selected card **drops the inverted accent-fill treatment** and instead draws the **normal
|
||||
cell** (Rest or Hover surface) + an **`accent/tertiary` border** (pastel purple `#C2AAE8`); the
|
||||
**waveform draws in its normal accent color** (the inverted `bg/base` wave is removed). The four
|
||||
grid-card interaction states stay **visually distinct and coherent:**
|
||||
- **Selected:** normal cell surface + **`accent/tertiary` (purple) 1px border**. No fill change,
|
||||
no wave inversion.
|
||||
- **Focus (caret):** the existing distinct inner ring — keep it separate from the selection
|
||||
border so a focused *and* selected card reads both (e.g. purple outer border + a
|
||||
`text/primary` inner focus ring). Settle the exact inner treatment at build so focus is
|
||||
legible on top of the selection border.
|
||||
- **Hover:** the kit Hover surface (unchanged) — a fill-state change, orthogonal to the purple
|
||||
border, so a hovered selected card still reads as selected.
|
||||
- **Drag-target slot:** the reorder drop-target highlight (a distinct accent — recommend
|
||||
`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)
|
||||
|
||||
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):**
|
||||
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).
|
||||
|
||||
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).
|
||||
|
||||
### 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.
|
||||
|
||||
## The L3 gate + Phase S coordination contract
|
||||
|
||||
**L3 (VST editor + embed-strip restyle) is GATED on Phase S landing on dev.** The VST
|
||||
|
||||
Reference in New Issue
Block a user