From e4bcc8f07517dbfe012ac07c9ad4e1c80b5f14d3 Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Mon, 27 Jul 2026 01:11:05 -0400 Subject: [PATCH] =?UTF-8?q?docs(product):=20spec=20Phase=20L=20L7=20?= =?UTF-8?q?=E2=80=94=20capture=20ordering,=20card=20metadata,=20and=20sele?= =?UTF-8?q?ction=20styling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- CONTEXT.md | 148 +++++++++++++++++++++++++++++++++++++++++++++++++++++ PLAN.md | 123 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 267 insertions(+), 4 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index 74a1bd2..7153b92 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -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 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 diff --git a/PLAN.md b/PLAN.md index 87af92b..9f456e1 100644 --- a/PLAN.md +++ b/PLAN.md @@ -372,17 +372,123 @@ work. state — never a pulse, per the speed constraint) as the signature surface; VST3 class UID unchanged. (Gated.) +## L7 — capture ordering, card metadata, and selection styling (ungated, after L6) +**Goal:** Three grid-facing improvements to the dock panel, drawn through the L1 kit in the +settled DS-2 palette. (1) **Persisted deterministic capture order + drag-drop reorder + sparse +placement:** each bank (and the pool) carries an explicit, persisted per-sample order, changed +by dragging cards within the grid; a card may sit in a grid slot that leaves gaps (an empty +first row above an occupied second row). (2) **Decorative metadata over the peaks:** each card +overlays capture length as **bars.beats.subdivisions in the bottom-LEFT** and **seconds.ms in +the bottom-RIGHT**, subtle/legible over the waveform in the kit's micro / value-mono type +classes. (3) **Selection restyle:** a selected card drops the inverted accent-fill and instead +draws the *normal* cell + an **`accent/tertiary` (pastel purple `#C2AAE8`) border**; hover, +focus, and drag-highlight stay visually distinct. **No new capture/placement behavior** — this +is model + layout + draw only; the "capture ≠ placement" principle is untouched. + +> **This is a persisted-model change, not pure layout.** Feature (1) adds an explicit +> 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. + +**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. +**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. +- [ ] 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 + **default = current insertion order, densely packed** (no gaps). Spec the default explicitly so + 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. +- [ ] **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). + +**(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 + encoded in a **pure decision helper** (mirror `drag_out::decideGesture`); shell reads live + pointer + region + client rect and calls it. +- [ ] 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). + +**(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 + `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. +- [ ] **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 + hit-test, no hover). Respects the speed constraint (no animation). + +**(d) Selection restyle (tertiary border):** +- [ ] `drawThumbnail`: a selected card draws the **normal cell fill** (Rest/Hover surface, not + `InteractionState::Active`) + an **`accent/tertiary` border** (pastel purple `#C2AAE8`); the + waveform draws in its normal accent color (drop the inverted `bg/base` wave). Focus ring, hover + state, and drag-target highlight stay **distinct** from the selection border (keep them + coherent — spec the four states' visual separation in CONTEXT.md §L7). + ## Phase L — sequencing ``` L1 (shared kit) ──► L2 (dock-panel layout redesign) [LANDED] ├─────────► L3 (VST editor + embed-strip restyle; GATED on Phase S landing on dev) └─────────► L4 (dock-panel button layout enhancement) [LANDED] └────► L5 (dock-panel button refinements; ungated, after L4) [LANDED] - -Phase S (separate team, ungated): S-work builds with current drawing, adopts L1 when it lands. + └────► L6 (toolbar polish; ungated, after L5) [LANDED] + └────► L7 (capture ordering + card metadata + selection styling; ungated, after L6) ``` -L1, L2, L4, and L5 have landed. L3 waits for Phase S to reach dev. -Phase S feature work runs in parallel, ungated by Phase L. +L1, L2, L4, L5, and L6 have landed. L7 is the next ungated dock-panel work. L3 waits for +Phase S to reach dev. Phase S feature work runs in parallel, ungated by Phase L. ## Phase L — must-verify-before-build - **LICE design-kit surfaces (L1)** — `LICE_GradRect`, `LICE_RoundRect`, AA @@ -407,6 +513,15 @@ 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. ---