FB2: Zone panel adopts the r11 knob deck + curve popup (R11-F2 parity) — slider rows and inline curve box retired, zone-authoring affordances kept

This commit is contained in:
2026-07-27 23:58:07 -04:00
parent a66acb7473
commit 682c21509a
2 changed files with 283 additions and 365 deletions
+71 -58
View File
@@ -33,8 +33,7 @@
#include "editor_geometry.h" // Rect (the shell's sub-rect type, shared with the pure modules)
#include "envelope_edit.h" // EnvClampBounds / NodeHit (S-VIEW-3 envelope node hit-test/edit)
#include "envelope_overlay.h" // AmpEnvelope / EnvNode (S-VIEW-3 envelope overlay draw seam)
#include "knob_deck.h" // DeckGroupDesc / DeckLayout (r11 Sample-face knob deck, FB1)
#include "param_slider.h" // ControlRow + the FA4 radial-knob primitive (S12/S15/S16 + r11)
#include "knob_deck.h" // DeckGroupDesc / DeckLayout (r11 knob deck — Sample FB1, Zone FB2)
#include "peaks.h" // Envelope (the cached peak thumbnail)
#include "sample_map.h" // SampleChoice, BankChoice, PerformanceMap (the shell's snapshot)
#include "velocity_curve.h" // VelocityCurve (S-VIEW-10 transfer-curve editor state)
@@ -82,14 +81,15 @@ private:
// draggable envelope breakpoint on the Sample-view hero overlay (S-VIEW-3, which node in
// envNode_); kCurveNode is a draggable velocity-curve control point in the S-VIEW-10
// transfer-curve editor (which point in curvePointIndex_); kDeckKnob is a GRAB-ANCHORED
// vertical radial-knob drag on the r11 Sample-face deck/cluster (which control in
// dragParamId_; the value at grab in dragKnobStartValue_ — no jump on grab, FA4).
// vertical radial-knob drag on an r11 knob deck — the Sample face's deck/cluster (FB1)
// or the Zone panel's per-zone deck (FB2) — (which control in dragParamId_; the value at
// grab in dragKnobStartValue_ — no jump on grab, FA4).
enum class DragKind { kNone, kRootMarker, kZoneLow, kZoneHigh, kZoneBody, kWaveMarker,
kScrollThumb, kParamSlider, kEnvNode, kCurveNode, kDeckKnob };
kScrollThumb, kEnvNode, kCurveNode, kDeckKnob };
// The parameter controls on the setup surface (S12 AHDSR + the S15/S16 control surfaces).
// The int value is the ControlDesc id the pure param_slider hit-test returns; the shell
// maps it to the picked zone's play params. Order here is the panel's top-down stack order.
// The int value is the opaque control id the pure knob_deck hit-test returns; the shell
// maps it to the picked zone's play params (or a processor-side per-instance setter).
enum class ParamControl {
kPlayMode = 0, // Gate | Trigger toggle (S15)
kPitchEngine, // Varispeed | Preserve toggle (S16)
@@ -162,14 +162,20 @@ private:
void paintBrowse(LICE_IBitmap* bmp, int w, int h); // S-VIEW-5 modal picker overlay
void paintZone(LICE_IBitmap* bmp, int w, int h); // S-VIEW-8 zone surface
void paintEmptyState(LICE_IBitmap* bmp, const Rect& area);
void paintControls(LICE_IBitmap* bmp, const Rect& panel, const PerformanceZone& zone); // S12/S15/S16 + keyTrack (Zone surface; the Sample face uses the r11 knob deck)
// --- r11 Sample-face recomposition (FB1) ---------------------------------------
// The knob deck: the fenced task groups (AMP ENVELOPE / PITCH / PITCH ENV / VOICE /
// MASTER) drawn through the L1 kit — group fence + caption + compact caption toggles +
// radial knobs (param_slider's FA4 primitive) with label<->value swap on hover/drag.
void paintKnobDeck(LICE_IBitmap* bmp, const Rect& deckArea, const PerformanceZone& zone);
// The centered curve-popup sheet (wash + title + close + full-size curve editor).
// --- r11 knob-deck rendering (FB1 Sample face; FB2 Zone panel) -------------------
// The knob deck: the fenced task groups drawn through the L1 kit — group fence + caption +
// compact caption toggles + radial knobs (param_slider's FA4 primitive) with label<->value
// swap on hover/drag. `descs` picks the group set: the full Sample deck (deckGroupDescs)
// or the Zone panel's per-zone groups (zoneDeckGroupDescs). Lays out from deckArea's
// top-left; the caller anchors (Sample bottom-anchors, Zone top-anchors).
void paintKnobDeck(LICE_IBitmap* bmp, const Rect& deckArea, const PerformanceZone& zone,
const std::vector<DeckGroupDesc>& descs);
// The mini curve-preview button (shared by the Sample cluster + the Zone panel, FB2): a
// hairline bg/cell square tracing the zone's live curve; Active border while the popup is up.
void paintCurveButton(LICE_IBitmap* bmp, const Rect& r, const PerformanceZone& zone);
// The centered curve-popup sheet (wash + title + close + full-size curve editor). Edits
// popupZone() — the Sample face's one-zone site or the Zone surface's selected zone (FB2).
void paintCurvePopup(LICE_IBitmap* bmp, int w, int h);
// Trace the S-VIEW-3 amp-envelope overlay + its draggable node handles over `waveArea` for
// `zone`'s play params, at the sample's wall-clock duration. Shared by the Sample hero band.
@@ -177,11 +183,10 @@ private:
std::int64_t frames);
// S-VIEW-10: the velocity->amp transfer-curve editor — a bordered box (X = velocity 0-127,
// Y = amp 0-1), the monotone spline traced by eval, one draggable node handle per control
// point. Shared by the Sample face (beside the hero) and the Zone param panel; all mapping /
// hit-test / clamp math lives in the pure velocity_curve module. `r` empty -> draws nothing.
// drawCaption: false suppresses the "Vel curve" corner label (the popup draws its own title).
void paintVelocityCurve(LICE_IBitmap* bmp, const Rect& r, const PerformanceZone& zone,
bool drawCaption = true);
// point. Since FB2 its ONLY host is the r11 popup sheet (both surfaces summon it via the
// mini preview button); all mapping / hit-test / clamp math lives in the pure
// velocity_curve module. `r` empty -> draws nothing.
void paintVelocityCurve(LICE_IBitmap* bmp, const Rect& r, const PerformanceZone& zone);
// Route a mouse-down inside curve-editor box `r` editing map_.zones[zoneIndex]: a node grab
// starts a kCurveNode drag; Alt-click on an interior node deletes it (committed at once);
@@ -189,23 +194,25 @@ private:
// `zoneIndex` must be a valid index into map_.zones (callers materialize first).
void handleCurveMouseDown(const Rect& r, int zoneIndex, int x, int y);
// Route a left-click while the curve popup is open (the popup is MODAL over the Sample
// face AND the Zone surface, FB2): Close / outside-wash dismiss, in-box clicks into the
// shared curve machinery against popupZoneIndex(), everything else on the sheet swallowed.
// Returns true when the popup consumed the click (i.e. whenever it is open).
bool handlePopupMouseDown(int w, int h, int x, int y);
void onMouseDown(int x, int y);
void onMouseMove(int x, int y);
void onMouseUp(int x, int y);
// r11: right-click — the curve popup's PRIMARY node-delete affordance (issue 3c). Only
// acts while the popup is open; a right-click on a popup curve node deletes it through
// the same commit path as Alt-click (deletePoint's endpoint guard makes endpoint
// right-clicks a safe no-op). Everything else ignores right-clicks.
// acts while the popup is open (over the Sample face OR the Zone surface, FB2); a
// right-click on a popup curve node deletes it through the same commit path as Alt-click
// (deletePoint's endpoint guard makes endpoint right-clicks a safe no-op). Everything
// else ignores right-clicks.
void onMouseRDown(int x, int y);
// Route a click at (x,y) into the param control panel `panel` editing map_.zones[zoneIndex]:
// a toggle segment commits immediately, a slider grab starts a live param-drag (kParamSlider),
// the keyTrack slider likewise (against the zone's keyTrack scalar). Returns true when the
// click landed on a control (handled). `zoneIndex` must be a valid index into map_.zones.
bool handleControlClick(int zoneIndex, const Rect& panel, int x, int y);
// Apply a slider/toggle interaction to map_.zones[zoneIndex] for control `id`: routes ordinary
// Apply a knob/toggle interaction to map_.zones[zoneIndex] for control `id`: routes ordinary
// controls through applyControl against the zone's play struct, and kKeyTrack against the
// zone's keyTrack scalar (0..200% over the slider's 0..1). Used by both the click + drag paths.
// zone's keyTrack scalar (0..200% over the knob's 0..1). Used by both the click + drag paths.
void applyZoneControl(int zoneIndex, int id, double value, int segment);
// Resolve the interactive element under (x, y) into hover_ (Phase L, L3). Called from
@@ -296,24 +303,18 @@ private:
// (0-based) that was updated or appended, so callers can set selectedZone_.
int upsertPickedOverride(const SetupMarkers& m);
// --- S12/S15/S16 parameter surface (Zones panel, keyed to selectedZone_) ------
// --- S12/S15/S16 parameter value domains (both deck surfaces) ------------------
//
// The control panel edits the SELECTED zone's ZonePlaySeconds (S15 play mode + AHDSR; S16
// pitch engine + AD pitch envelope). Wall-clock times are SECONDS (rate-free); the keymap
// build resolves them to frames at the live rate. Instrument-owned (D-B), never a bank fact.
// The control descriptors the panel shows for `play`'s CURRENT play mode: the two toggles +
// the mode-relevant sliders (AHDSR for Gate, %-length/fades for Trigger) + the pitch-envelope
// controls. The pure param_slider lays these out; this only picks the set. Static (a free
// choice of set from the mode) — kept a member for the ParamControl enum access.
std::vector<ControlDesc> controlDescs(const ZonePlaySeconds& play) const;
// The deck knobs edit a zone's ZonePlaySeconds (S15 play mode + AHDSR; S16 pitch engine +
// AD pitch envelope). Wall-clock times are SECONDS (rate-free); the keymap build resolves
// them to frames at the live rate. Instrument-owned (D-B), never a bank fact.
// The normalized [0,1] display value for control `id` given `play` (the shell's domain
// mapping: seconds->0..1 over a fixed seconds ceiling, sustain 0..1 as-is, %-length/fade
// frames->0..1, semitone depth centered at 0.5).
double controlValue(int id, const ZonePlaySeconds& play) const;
// Apply a committed control interaction to `play`: a slider's normalized `value` (mapped back
// Apply a committed control interaction to `play`: a knob's normalized `value` (mapped back
// into the control's stored domain) or a toggle's `segment` (0/1). Mutates `play` in place.
void applyControl(int id, ZonePlaySeconds& play, double value, int segment) const;
@@ -364,18 +365,31 @@ private:
// needs a concrete zone to write. Returns -1 if selectedId_ is empty.
int ensureSampleZone();
// --- r11 knob-deck plumbing (FB1) ---------------------------------------------
// --- Curve-popup target resolution (r11 FB1 + FB2) -----------------------------
//
// The deck is the r11 replacement for the Sample face's slider control strip: the pure
// knob_deck module lays out the fenced groups, param_slider's FA4 primitive owns the
// value<->needle map, and these members own the control-id <-> value binding (the same
// division of labor paintControls/applyControl use for the Zone surface's sliders).
// The popup edits ONE zone per open: the Zone surface's SELECTED zone (FB2) or the Sample
// face's picked one-zone site. popupZone is the read-only resolve (paint/hover/right-click
// hit-test); popupZoneIndex is the edit target — it materializes the Sample-face zone via
// ensureSampleZone but NEVER materializes on the Zone surface (the button only shows for
// an explicit selection). Returns -1 when there is no valid target (callers guard).
PerformanceZone popupZone() const;
int popupZoneIndex();
// The deck group descriptors for the current mode: AMP ENVELOPE (Gate: A/H/D/S/R;
// Trigger: Fade In / Length % / Fade Out + two RESERVED blanks so a mode flip never
// reflows the neighbours) / PITCH (Key Track) / PITCH ENV (P.Attack/P.Decay/P.Depth) /
// VOICE (Voices knob + Poly|Mono caption toggle + Retrig|Legato row toggle) / MASTER
// (the FB1 post-mixer Gain knob).
// --- r11 knob-deck plumbing (FB1 Sample face; FB2 Zone panel) -------------------
//
// The deck is the r11 replacement for the slider control strips on BOTH surfaces: the pure
// knob_deck module lays out the fenced groups, param_slider's FA4 primitive owns the
// value<->needle map, and these members own the control-id <-> value binding.
// The PER-ZONE deck groups (FB2 — the set both surfaces share): AMP ENVELOPE (Gate:
// A/H/D/S/R; Trigger: Fade In / Length % / Fade Out + two RESERVED blanks so a mode flip
// never reflows the neighbours) / PITCH (Key Track) / PITCH ENV (P.Attack/P.Decay/P.Depth).
// The Zone panel renders exactly these — per-instance state stays off it.
std::vector<DeckGroupDesc> zoneDeckGroupDescs(const ZonePlaySeconds& play) const;
// The full Sample-face deck: the shared per-zone groups + the per-instance VOICE (Voices
// knob + Poly|Mono caption toggle + Retrig|Legato row toggle) and MASTER (the FB1
// post-mixer Gain knob) groups.
std::vector<DeckGroupDesc> deckGroupDescs(const ZonePlaySeconds& play) const;
// The normalized [0,1] value a deck knob shows for `zone` — zone params route through
@@ -480,12 +494,10 @@ private:
std::int64_t dragStartFrame_ = 0; // zone startPoint at grab time (0 if absent); for env-node drag
// S12 scrollbar-thumb drag: the offset held at grab time (the pixel-delta resolver shifts
// from it). S12/S15/S16 param-slider drag: which control id + the panel it lives in (the
// shell re-lays the panel each move to map x->value against the live control rect).
// from it). kDeckKnob drag: which control id + the zone it edits.
int dragStartScrollOffset_ = 0;
int dragParamId_ = -1; // ControlDesc id under a kParamSlider drag; -2 = preview-vel knob
int dragParamZone_ = -1; // the zone index a kParamSlider drag edits (Sample or Zone face)
Rect dragParamPanel_{};
int dragParamId_ = -1; // control id under a kDeckKnob drag; -2 = preview-vel knob
int dragParamZone_ = -1; // the zone index a kDeckKnob drag edits; -1 = processor-side
// S-VIEW-3 envelope-node drag: which node is grabbed + the AmpEnvelope snapshotted at grab
// (so the pixel delta is absolute, per envelope_edit's grabEnv contract). The overlay rect +
@@ -506,9 +518,10 @@ private:
// the vertical pixel delta from this anchor, so a grab never jumps the value (FA4).
double dragKnobStartValue_ = 0.0;
// r11 curve popup (FB1): open flag — editor-local, never persisted. The popup edits the
// picked capture's one-zone site (effectiveSampleZone / ensureSampleZone), re-resolved
// each paint so a sync-tick refresh mid-open stays coherent.
// r11 curve popup (FB1 + FB2): open flag — editor-local, never persisted. The popup edits
// popupZone() — the picked capture's one-zone site on the Sample face, the SELECTED zone
// on the Zone surface — re-resolved each paint so a sync-tick refresh mid-open stays
// coherent (a refresh that drops the target closes it; see refreshFromBank).
bool curvePopupOpen_ = false;
// --- Peak-thumbnail cache (mirror of bank_panel; id -> envelope at a bin width) ------