diff --git a/src/vst/reasampler_editor.cpp b/src/vst/reasampler_editor.cpp index 6de3798..9304835 100644 --- a/src/vst/reasampler_editor.cpp +++ b/src/vst/reasampler_editor.cpp @@ -23,7 +23,7 @@ #include "master_gain.h" // r11 master-gain dB<->linear<->knob taper (FB1) #include "theme.h" // Role / InteractionState / KitColor / spectralColor (L3) #include "note_entry.h" // S12 direct numeric note-entry parse -#include "param_slider.h" // S12/S15/S16 control-surface layout + value<->pixel mapping +#include "param_slider.h" // the FA4 radial-knob primitive (value<->needle map, drag delta) #include "peaks.h" // computeEnvelope #include "reaper_bridge.h" #include "reasampler_processor.h" @@ -174,6 +174,9 @@ void ReaSamplerEditor::refreshFromBank() { // curve popup — the empty-state Sample face no longer draws it, and an open-but-invisible // modal would swallow clicks. if (selectedId_.empty() && map_.zones.empty()) curvePopupOpen_ = false; + // FB2: on the Zone surface the popup edits the SELECTED zone; if the refresh dropped the + // selection (the zones list shrank), close it rather than let it retarget another zone. + if (view_ == View::kZone && selectedZone_ < 0) curvePopupOpen_ = false; // Drop a filter that names a bank no longer present. if (!activeFilterBankId_.empty()) { bool found = false; @@ -373,34 +376,6 @@ constexpr double kKeyTrackMax = 2.0; // S-VIEW-6 key-track slide double clamp01(double v) { return v < 0.0 ? 0.0 : (v > 1.0 ? 1.0 : v); } } // namespace -std::vector ReaSamplerEditor::controlDescs(const ZonePlaySeconds& play) const { - std::vector out; - // Always: the two mode toggles. - out.push_back({static_cast(ParamControl::kPlayMode), ControlKind::Toggle}); - out.push_back({static_cast(ParamControl::kPitchEngine), ControlKind::Toggle}); - // Mode-relevant amplitude sliders. - if (play.playMode == PlayMode::Gate) { - out.push_back({static_cast(ParamControl::kAttack), ControlKind::Slider}); - out.push_back({static_cast(ParamControl::kHold), ControlKind::Slider}); - out.push_back({static_cast(ParamControl::kDecay), ControlKind::Slider}); - out.push_back({static_cast(ParamControl::kSustain), ControlKind::Slider}); - out.push_back({static_cast(ParamControl::kRelease), ControlKind::Slider}); - } else { // Trigger - out.push_back({static_cast(ParamControl::kTrigLength), ControlKind::Slider}); - out.push_back({static_cast(ParamControl::kTrigFadeIn), ControlKind::Slider}); - out.push_back({static_cast(ParamControl::kTrigFadeOut), ControlKind::Slider}); - } - // The AD pitch envelope: an enable toggle + its three sliders (drawn always; inert until on). - out.push_back({static_cast(ParamControl::kPitchEnvEnable), ControlKind::Toggle}); - out.push_back({static_cast(ParamControl::kPitchEnvAttack), ControlKind::Slider}); - out.push_back({static_cast(ParamControl::kPitchEnvDecay), ControlKind::Slider}); - out.push_back({static_cast(ParamControl::kPitchEnvDepth), ControlKind::Slider}); - // S-VIEW-6 key-tracking (0..200%). Lives on PerformanceZone, not ZonePlaySeconds — the shell - // reads/writes it against the zone directly (controlValue/applyControl ignore it). Always shown. - out.push_back({static_cast(ParamControl::kKeyTrack), ControlKind::Slider}); - return out; -} - double ReaSamplerEditor::controlValue(int id, const ZonePlaySeconds& play) const { // Wall-clock seconds -> normalized over the seconds ceiling; source frames -> normalized over // the frames ceiling. Two domains, kept explicit so neither leaks a rate. @@ -487,12 +462,12 @@ enum DeckGroup { }; } // namespace -std::vector ReaSamplerEditor::deckGroupDescs(const ZonePlaySeconds& play) const { +std::vector ReaSamplerEditor::zoneDeckGroupDescs(const ZonePlaySeconds& play) const { + // The PER-ZONE groups — the deck grammar both surfaces share (FB2: the Zone panel renders + // exactly these; the Sample face appends the per-instance groups in deckGroupDescs). // Group widths are MODE-INDEPENDENT: AMP ENVELOPE reserves its 5-cell Gate width (Trigger // leaves two blank cells), so a Gate<->Trigger flip repopulates in place and never reflows - // the neighbouring groups (r11). The VOICE + MASTER groups are the FB1 homes for the - // provisional voice-deck controls and the new post-mixer gain — the r11 spec predates - // both; surfaced as a judgment call in the handoff. + // the neighbouring groups (r11). std::vector out; { DeckGroupDesc amp; @@ -532,6 +507,15 @@ std::vector ReaSamplerEditor::deckGroupDescs(const ZonePlaySecond static_cast(ParamControl::kPitchEnvDepth)}; out.push_back(std::move(penv)); } + return out; +} + +std::vector ReaSamplerEditor::deckGroupDescs(const ZonePlaySeconds& play) const { + // The full Sample-face deck: the shared per-zone groups + the per-instance VOICE + MASTER + // groups. VOICE + MASTER are the FB1 homes for the provisional voice-deck controls and the + // post-mixer gain — the r11 spec predates both; per-instance state (ComponentState) stays + // OFF the Zone panel (FB2), so they are appended here, not in zoneDeckGroupDescs. + std::vector out = zoneDeckGroupDescs(play); { DeckGroupDesc voice; voice.id = kGroupVoice; @@ -888,12 +872,11 @@ tresult PLUGIN_API ReaSamplerEditor::onSize(ViewRect* newSize) { namespace { constexpr int kPad = 8; -// The S-VIEW-10 velocity-curve editor box metrics. Since r11 the Sample face hosts the curve in -// a POPUP (curve_popup) summoned from the mini preview button; the Zone param panel still draws -// the inline box (FB2 owns Zone parity). The INSET keeps node handles + the pick radius inside -// the border so an endpoint at amp 0/1 stays grabbable — the ONE curveBoxFromRect grammar every -// curve surface (Zone inline box AND the r11 popup) derives its mapping box through. -constexpr int kVelCurveBoxW = 168; // the Zone inline curve box width (incl. border) +// The S-VIEW-10 velocity-curve editor box metrics. Since r11/FB2 BOTH surfaces host the curve +// in the POPUP (curve_popup), each summoned from its own mini preview button — the Sample +// cluster's and the Zone panel's (the inline Zone box is retired). The INSET keeps node handles +// + the pick radius inside the border so an endpoint at amp 0/1 stays grabbable — the ONE +// curveBoxFromRect grammar the popup derives its mapping box through. constexpr int kVelCurveInset = 14; // border -> mapping-box inset: caption band (~12px) + 2px gap constexpr int kCurveDragOffMargin = 24; // release beyond box+margin -> drag-off delete @@ -1010,25 +993,21 @@ Rect zonesControlPanel(const Rect& content) { content.bottom - 4}; } -// The S-VIEW-10 velocity-curve editor inside the Zone param panel: right-anchored, top-aligned, -// with the control rows keeping the panel's left. Suppressed (empty rect) when the panel is too -// narrow/short for both — the controls then keep the full panel (the pre-S-VIEW-10 layout). -// Both draw + hit-test derive from these two formulas so they never drift. -constexpr int kVelCurveBoxH = 140; // the Zone-panel curve box height (Sample uses the hero's) -constexpr int kVelCurveMinPanelW = 220; // controls keep at least this much width beside the box -Rect zonesCurveBox(const Rect& content) { +// FB2 (R11-F2 parity): the Zone panel's per-zone controls render as the SAME knob deck the +// Sample face uses. The deck lays out from the panel top (top-anchored — the Zone panel reads +// top-down, unlike the Sample face's bottom-anchored band), with a column at the panel's right +// reserved for the mini curve-preview button so a deck row can never collide with it (the pure +// knob_deck wrap keeps whole groups inside availWidth). Both draw + hit-test derive from these +// two formulas so they never drift. +Rect zonesDeckArea(const Rect& content) { const Rect panel = zonesControlPanel(content); - if (panel.width() < kVelCurveBoxW + kPad + kVelCurveMinPanelW || panel.height() < 80) { - return Rect{}; - } - const int boxH = (std::min)(kVelCurveBoxH, panel.height()); - return Rect{panel.right - kVelCurveBoxW, panel.top, panel.right, panel.top + boxH}; + return Rect{panel.left, panel.top, panel.right - kCurveBtnSize - kPad, panel.bottom}; } -Rect zonesControlsArea(const Rect& content) { +// The Zone panel's mini curve-preview button (opens the SAME popup editor as the Sample +// cluster's button): the cluster's 28px square, right-anchored at the panel top. +Rect zonesCurveButton(const Rect& content) { const Rect panel = zonesControlPanel(content); - const Rect curve = zonesCurveBox(content); - if (curve.width() <= 0) return panel; - return Rect{panel.left, panel.top, curve.left - kPad, panel.bottom}; + return Rect{panel.right - kCurveBtnSize, panel.top, panel.right, panel.top + kCurveBtnSize}; } // The pure-module mapping Box for a drawn curve rect: inset from the border so node handles and @@ -1333,36 +1312,9 @@ void ReaSamplerEditor::paintSample(LICE_IBitmap* bmp, int w, int h) { kitTextCentered(bmp, cr.velLabel, "Vel", Font::Micro, Role::TextDim); } } - // The mini curve-preview button (r11): a hairline-bordered bg/cell square with the zone's - // live velocity curve traced in miniature (no node markers at this scale). Hover lifts it; - // it draws ACTIVE (accent-primary border) while its popup is open, and re-renders live as - // the popup edits the curve (same zone, re-read each paint). - { - const bool hov = isHovered(HoverKind::kCurveButton, -1); - fillSurface(bmp, toKitBox(cr.curveBtn), Role::BgCell, - hov ? InteractionState::Hover : InteractionState::Rest); - const KitColor border = curvePopupOpen_ ? roleColor(Role::AccentPrimary) - : roleColor(Role::LineHairline); - LICE_DrawRect(bmp, cr.curveBtn.left, cr.curveBtn.top, cr.curveBtn.width() - 1, - cr.curveBtn.height() - 1, toLice(border), 1.0f, 0); - const VelocityCurve& curve = zone.velocityCurve; - const int inset = 3; - const VelocityCurve::Box mini{cr.curveBtn.left + inset, cr.curveBtn.top + inset, - cr.curveBtn.width() - 2 * inset, - cr.curveBtn.height() - 2 * inset}; - if (mini.width > 1 && mini.height > 1) { - const LICE_pixel trace = toLice(roleColor(Role::AccentSecondary)); - int prevX = 0, prevY = 0; - for (int px = 0; px <= mini.width; ++px) { - const int mx = mini.left + px; - const double vel = VelocityCurve::pointFromPixel(mini, mx, mini.top).velocity; - const int my = VelocityCurve::pixelFromPoint(mini, {vel, curve.eval(vel)}).y; - if (px > 0) LICE_Line(bmp, prevX, prevY, mx, my, trace, 1.0f, 0, true); - prevX = mx; - prevY = my; - } - } - } + // The mini curve-preview button (r11): opens the popup editor. Shared painter with the + // Zone panel's button (FB2 — one grammar on both surfaces). + paintCurveButton(bmp, cr.curveBtn, zone); // Mono | Stereo output-mode toggle. { const bool isStereo = (channelMode_ == ChannelMode::Stereo); @@ -1377,7 +1329,7 @@ void ReaSamplerEditor::paintSample(LICE_IBitmap* bmp, int w, int h) { } // --- The knob deck (r11: the fenced control groups, bottom-anchored) ------------------- - paintKnobDeck(bmp, bands.deck, zone); + paintKnobDeck(bmp, bands.deck, zone, deckDescs); // --- The curve popup (r11): a centered sheet over the whole Sample face, drawn LAST ---- if (curvePopupOpen_) paintCurvePopup(bmp, w, h); @@ -1419,19 +1371,15 @@ void ReaSamplerEditor::paintEnvelopeOverlay(LICE_IBitmap* bmp, const Rect& waveA } void ReaSamplerEditor::paintVelocityCurve(LICE_IBitmap* bmp, const Rect& r, - const PerformanceZone& zone, - bool drawCaption) { - if (r.width() <= 0 || r.height() <= 0) return; // suppressed (window too narrow) + const PerformanceZone& zone) { + if (r.width() <= 0 || r.height() <= 0) return; // defensive (degenerate rect) - // The bordered box: a panel surface + hairline border, drawn by palette role. + // The bordered box: a panel surface + hairline border, drawn by palette role. No corner + // caption — the popup sheet's own "VELOCITY -> AMP" title labels this context (FB2: the + // popup is the only host). fillSurface(bmp, toKitBox(r), Role::BgPanel, InteractionState::Rest); LICE_DrawRect(bmp, r.left, r.top, r.width() - 1, r.height() - 1, toLice(roleColor(Role::LineHairline)), 1.0f, 0); - // A corner caption (decorative — the axes are velocity -> amp). Suppressed when the caller - // (e.g. the curve popup) renders its own sheet title so the label doesn't double. - if (drawCaption) - kitText(bmp, Rect{r.left + 4, r.top + 1, r.right - 4, r.top + 12}, "Vel curve", - Font::Micro, Role::TextDim); const VelocityCurve::Box box = curveBoxFromRect(r); if (box.width <= 0 || box.height <= 1) return; @@ -1479,9 +1427,9 @@ void ReaSamplerEditor::paintVelocityCurve(LICE_IBitmap* bmp, const Rect& r, } void ReaSamplerEditor::paintKnobDeck(LICE_IBitmap* bmp, const Rect& deckArea, - const PerformanceZone& zone) { + const PerformanceZone& zone, + const std::vector& descs) { if (deckArea.width() <= 0 || deckArea.height() <= 0) return; - const std::vector descs = deckGroupDescs(zone.play); const DeckLayout dl = layoutDeck(descs, deckArea.left, deckArea.top, deckArea.width()); const ZonePlaySeconds& play = zone.play; const bool isMono = (voiceMode_ == VoiceMode::Mono); @@ -1594,6 +1542,37 @@ void ReaSamplerEditor::paintKnobDeck(LICE_IBitmap* bmp, const Rect& deckArea, } } +void ReaSamplerEditor::paintCurveButton(LICE_IBitmap* bmp, const Rect& r, + const PerformanceZone& zone) { + // The mini curve-preview button (r11/FB2 — shared by the Sample cluster and the Zone + // panel): a hairline-bordered bg/cell square with the zone's live velocity curve traced + // in miniature (no node markers at this scale). Hover lifts it; it draws ACTIVE + // (accent-primary border) while its popup is open, and re-renders live as the popup + // edits the curve (same zone, re-read each paint). + const bool hov = isHovered(HoverKind::kCurveButton, -1); + fillSurface(bmp, toKitBox(r), Role::BgCell, + hov ? InteractionState::Hover : InteractionState::Rest); + const KitColor border = curvePopupOpen_ ? roleColor(Role::AccentPrimary) + : roleColor(Role::LineHairline); + LICE_DrawRect(bmp, r.left, r.top, r.width() - 1, r.height() - 1, toLice(border), 1.0f, 0); + const VelocityCurve& curve = zone.velocityCurve; + const int inset = 3; + const VelocityCurve::Box mini{r.left + inset, r.top + inset, r.width() - 2 * inset, + r.height() - 2 * inset}; + if (mini.width > 1 && mini.height > 1) { + const LICE_pixel trace = toLice(roleColor(Role::AccentSecondary)); + int prevX = 0, prevY = 0; + for (int px = 0; px <= mini.width; ++px) { + const int mx = mini.left + px; + const double vel = VelocityCurve::pointFromPixel(mini, mx, mini.top).velocity; + const int my = VelocityCurve::pixelFromPoint(mini, {vel, curve.eval(vel)}).y; + if (px > 0) LICE_Line(bmp, prevX, prevY, mx, my, trace, 1.0f, 0, true); + prevX = mx; + prevY = my; + } + } +} + void ReaSamplerEditor::paintCurvePopup(LICE_IBitmap* bmp, int w, int h) { // The 0.50-alpha bg/base wash (lighter than Browse's 0.82 — a focused sub-editor; the // Sample face stays legible behind it), then the centered sheet. @@ -1610,11 +1589,58 @@ void ReaSamplerEditor::paintCurvePopup(LICE_IBitmap* bmp, int w, int h) { : InteractionState::Rest; drawButton(bmp, box, "x", st, /*warn=*/false); } - // The full-size editor: the SAME draw path as the Zone inline box (paintVelocityCurve + - // the one curveBoxFromRect mapping formula), so trace/handles/drag-off cues cannot drift - // between the two surfaces. The popup edits the picked capture's one-zone site. Caption - // suppressed (the sheet's own "VELOCITY -> AMP" title above is the label for this context). - paintVelocityCurve(bmp, pl.curveBox, effectiveSampleZone(), /*drawCaption=*/false); + // The full-size editor: ONE draw path + the one curveBoxFromRect mapping formula, so + // trace/handles/drag-off cues cannot drift between hosts. The popup edits popupZone() — + // the picked capture's one-zone site on the Sample face, the selected zone on the Zone + // surface (FB2). + paintVelocityCurve(bmp, pl.curveBox, popupZone()); +} + +PerformanceZone ReaSamplerEditor::popupZone() const { + // The zone the popup displays: the Zone surface's SELECTED zone (FB2), else the Sample + // face's one-zone site (a read-only resolve — an edit materializes via popupZoneIndex). + if (view_ == View::kZone && selectedZone_ >= 0 && + selectedZone_ < static_cast(map_.zones.size())) { + return map_.zones[static_cast(selectedZone_)]; + } + return effectiveSampleZone(); +} + +int ReaSamplerEditor::popupZoneIndex() { + // The map_.zones index a popup edit lands on, or -1 when there is no valid target. The + // Zone surface never materializes (the button only shows for an explicit selection); the + // Sample face finds-or-materializes the picked id's one-zone site. + if (view_ == View::kZone) { + return (selectedZone_ >= 0 && selectedZone_ < static_cast(map_.zones.size())) + ? selectedZone_ + : -1; + } + return ensureSampleZone(); +} + +bool ReaSamplerEditor::handlePopupMouseDown(int w, int h, int x, int y) { + // The r11 curve popup: while open the sheet is MODAL over its host face — the Sample home + // (FB1) or the Zone surface (FB2) — it owns every left-click. Close click / outside-wash + // click dismiss (outside only when no drag is in flight, per the spec); in-box clicks + // route to the shared curve machinery against popupZoneIndex(); anything else on the + // sheet is swallowed. + if (!curvePopupOpen_) return false; + const CurvePopupLayout pl = computeCurvePopup(w, h); + if (contains(pl.close, x, y)) { + curvePopupOpen_ = false; + invalidate(); + return true; + } + if (contains(pl.curveBox, x, y)) { + const int zi = popupZoneIndex(); + if (zi >= 0) handleCurveMouseDown(pl.curveBox, zi, x, y); + return true; + } + if (popupOutsideSheet(pl, x, y) && drag_ == DragKind::kNone) { + curvePopupOpen_ = false; + invalidate(); + } + return true; } void ReaSamplerEditor::handleCurveMouseDown(const Rect& r, int zoneIndex, int x, int y) { @@ -1955,104 +1981,20 @@ void ReaSamplerEditor::paintZone(LICE_IBitmap* bmp, int w, int h) { Font::Label, Role::TextDim); } - // The S12/S15/S16 parameter surface for the selected zone (play mode + AHDSR / Trigger + - // pitch engine + AD pitch envelope). Shown for an explicit zone selection OR for the - // single-capture face when the map is empty but a capture is picked (S15-F2 lean: the - // single capture is already a one-zone map — one storage site serves both). + // The per-zone parameter surface for the selected zone. FB2 (R11-F2): the SAME knob deck + + // curve-preview-button/popup grammar as the Sample face — one control language over the one + // storage site (S15-F2) — replacing the retired param_slider rows + inline curve box. Only + // the per-zone groups render here; VOICE/MASTER are per-instance (ComponentState) and live + // on the Sample deck only. if (selectedZone_ >= 0 && selectedZone_ < static_cast(map_.zones.size())) { const PerformanceZone& z = map_.zones[static_cast(selectedZone_)]; - paintControls(bmp, zonesControlsArea(content), z); - // S-VIEW-10: the same velocity-curve editor, right-anchored in the per-zone param panel - // (one curve per zone). Suppressed (empty rect) when the panel is too narrow. - paintVelocityCurve(bmp, zonesCurveBox(content), z); + paintKnobDeck(bmp, zonesDeckArea(content), z, zoneDeckGroupDescs(z.play)); + paintCurveButton(bmp, zonesCurveButton(content), z); } -} -// The label + the two toggle-segment captions for a control (member so it can name the private -// ParamControl enum). Segments are only read for a ControlKind::Toggle. -namespace { -struct ControlLabels { const char* label; const char* seg0; const char* seg1; }; -} // namespace - -void ReaSamplerEditor::paintControls(LICE_IBitmap* bmp, const Rect& panel, - const PerformanceZone& zone) { - // The control panel edits `zone`: its ZonePlaySeconds (Mode/Pitch/AHDSR-or-Trigger/AD-pitch) - // plus the S-VIEW-6 keyTrack scalar (which lives on PerformanceZone, not the play struct — it - // is read/written from `zone` directly). Shared by the Sample home face (single-capture, the - // effective one-zone site) and the Zone surface (an explicit zone) — one storage site. - const ZonePlaySeconds& play = zone.play; - const std::vector descs = controlDescs(play); - const std::vector rows = layoutControls(panel, descs); - - const auto labelsFor = [](ParamControl c) -> ControlLabels { - switch (c) { - case ParamControl::kPlayMode: return {"Mode", "Gate", "Trigger"}; - case ParamControl::kPitchEngine: return {"Pitch eng", "Varisp", "Preserve"}; - case ParamControl::kAttack: return {"Attack", "", ""}; - case ParamControl::kHold: return {"Hold", "", ""}; - case ParamControl::kDecay: return {"Decay", "", ""}; - case ParamControl::kSustain: return {"Sustain", "", ""}; - case ParamControl::kRelease: return {"Release", "", ""}; - case ParamControl::kTrigLength: return {"Length %", "", ""}; - case ParamControl::kTrigFadeIn: return {"Fade in", "", ""}; - case ParamControl::kTrigFadeOut: return {"Fade out", "", ""}; - case ParamControl::kPitchEnvEnable: return {"Pitch env", "Off", "On"}; - case ParamControl::kPitchEnvAttack: return {"P.Attack", "", ""}; - case ParamControl::kPitchEnvDecay: return {"P.Decay", "", ""}; - case ParamControl::kPitchEnvDepth: return {"P.Depth", "", ""}; - case ParamControl::kKeyTrack: return {"Key track", "", ""}; - default: return {"", "", ""}; - } - }; - - for (const ControlRow& r : rows) { - if (r.row.top >= panel.bottom) break; // clip at the panel bottom - const ControlLabels lab = labelsFor(static_cast(r.id)); - kitText(bmp, r.label, lab.label, Font::Micro, Role::TextDim); - // kKeyTrack lives on the zone (0..200% over kKeyTrackMax), not in `play` — resolve it - // directly; every other control reads through controlValue against the play struct. - const double v = (r.id == static_cast(ParamControl::kKeyTrack)) - ? clamp01(zone.keyTrack / kKeyTrackMax) - : controlValue(r.id, play); - const bool hov = isHovered(HoverKind::kControl, r.id); - if (r.kind == ControlKind::Toggle) { - const bool seg1 = (v >= 0.5); - const Rect s0 = toggleSegmentRect(r.control, 0); - const Rect s1 = toggleSegmentRect(r.control, 1); - // The lit segment carries the primary accent (Active); the unlit segment hovers - // toward accent/hot when the whole control is under the pointer. - const InteractionState s0State = - !seg1 ? InteractionState::Active : (hov ? InteractionState::Hover : InteractionState::Rest); - const InteractionState s1State = - seg1 ? InteractionState::Active : (hov ? InteractionState::Hover : InteractionState::Rest); - fillSurface(bmp, toKitBox(s0), Role::BgCell, s0State); - fillSurface(bmp, toKitBox(s1), Role::BgCell, s1State); - kitTextCentered(bmp, s0, lab.seg0, Font::Micro, !seg1 ? Role::BgBase : Role::TextPrimary); - kitTextCentered(bmp, s1, lab.seg1, Font::Micro, seg1 ? Role::BgBase : Role::TextPrimary); - } else { - // Track groove (recessed cell) + accent filled portion up to the handle + a raised - // handle. Dragging THIS control brightens the fill/handle (accent/hot). - const bool dragging = (drag_ == DragKind::kParamSlider && dragParamId_ == r.id); - const Rect track = sliderTrackRect(r.control); - fillSurface(bmp, toKitBox(Rect{track.left, track.top + track.height() / 2 - 1, - track.right, track.top + track.height() / 2 + 1}), - Role::BgCell, InteractionState::Pressed); - const Rect handle = sliderHandleRect(r.control, v); - // Filled portion: track-left to the handle center. - const int fillW = (std::max)(0, (handle.left + handle.width() / 2) - track.left); - if (fillW > 0) { - LICE_FillRect(bmp, track.left, track.top + track.height() / 2 - 1, fillW, 2, - toLice(roleColor(dragging ? Role::AccentHot : Role::AccentPrimary)), - 1.0f, 0); - } - const KitButtonBox knob{toKitBox(Rect{handle.left, handle.top + 2, handle.right, - handle.bottom - 2})}; - drawButton(bmp, knob, nullptr, - dragging ? InteractionState::Dragging - : (hov ? InteractionState::Hover : InteractionState::Rest), - /*warn=*/false); - } - } + // The curve popup (FB2): a centered sheet over the whole Zone surface, drawn LAST — + // the same modal grammar as the Sample face. + if (curvePopupOpen_) paintCurvePopup(bmp, w, h); } // --- Hover resolution (Phase L, L3) ------------------------------------------ @@ -2087,6 +2029,16 @@ void ReaSamplerEditor::resolveHover(int x, int y) { if (tab >= 0) h = {HoverKind::kFilterTab, tab}; else if (card >= 0) h = {HoverKind::kCard, card}; } + } else if (curvePopupOpen_) { // the r11 curve popup — modal over Sample AND Zone (FB2) + const CurvePopupLayout pl = computeCurvePopup(w, hgt); + if (contains(pl.close, x, y)) { + h = {HoverKind::kPopupClose, -1}; + } else if (contains(pl.curveBox, x, y)) { + // A curve node under the pointer lights accent-hot. + const int idx = + popupZone().velocityCurve.pointAtPixel(curveBoxFromRect(pl.curveBox), x, y); + if (idx >= 0) h = {HoverKind::kCurveNode, idx}; + } } else if (view_ == View::kZone) { const Rect back{w - kPad - kNavButtonWidth, 2, w - kPad, (std::max)(2, (std::min)(kTitleHeight, hgt) - 2)}; @@ -2100,33 +2052,20 @@ void ReaSamplerEditor::resolveHover(int x, int y) { } else if (selectedZone_ >= 0 && contains(delR, x, y)) { h = {HoverKind::kDeleteZone, -1}; } else if (selectedZone_ >= 0 && selectedZone_ < static_cast(map_.zones.size())) { - const Rect cb = zonesCurveBox(content); - if (cb.width() > 0 && contains(cb, x, y)) { - // S-VIEW-10: a curve node under the pointer lights accent-hot. - const PerformanceZone& z = map_.zones[static_cast(selectedZone_)]; - const int idx = z.velocityCurve.pointAtPixel(curveBoxFromRect(cb), x, y); - if (idx >= 0) h = {HoverKind::kCurveNode, idx}; + // FB2: the per-zone knob deck + the mini curve-preview button (the Sample deck's + // hover grammar — knobs light + swap label->value). + if (contains(zonesCurveButton(content), x, y)) { + h = {HoverKind::kCurveButton, -1}; } else { const ZonePlaySeconds& play = map_.zones[static_cast(selectedZone_)].play; - const Rect panel = zonesControlsArea(content); - const std::vector descs = controlDescs(play); - const std::vector rows = layoutControls(panel, descs); - const int id = controlAtPoint(rows, x, y); - if (id >= 0) h = {HoverKind::kControl, id}; + const Rect deckArea = zonesDeckArea(content); + const DeckLayout dl = layoutDeck(zoneDeckGroupDescs(play), deckArea.left, + deckArea.top, deckArea.width()); + const DeckHit dh = hitTestDeck(dl, x, y); + if (dh.kind != DeckHitKind::None) h = {HoverKind::kControl, dh.id}; } } - } else if (curvePopupOpen_) { // Sample view with the r11 curve popup open (modal sheet) - const CurvePopupLayout pl = computeCurvePopup(w, hgt); - if (contains(pl.close, x, y)) { - h = {HoverKind::kPopupClose, -1}; - } else if (contains(pl.curveBox, x, y)) { - // A curve node under the pointer lights accent-hot (mirror of the inline box). - const PerformanceZone zone = effectiveSampleZone(); - const int idx = - zone.velocityCurve.pointAtPixel(curveBoxFromRect(pl.curveBox), x, y); - if (idx >= 0) h = {HoverKind::kCurveNode, idx}; - } } else { // Sample view (home, r11 recomposition) const PerformanceZone zone = effectiveSampleZone(); const std::vector descs = deckGroupDescs(zone.play); @@ -2242,28 +2181,8 @@ void ReaSamplerEditor::onMouseDown(int x, int y) { // ---- Sample home (S-VIEW-2 / r11) ---- if (view_ == View::kSample) { - // r11 curve popup: while open the sheet is modal over the Sample face — it owns every - // left-click. Close click / outside-wash click dismiss (outside only when no drag is - // in flight, per the spec); in-box clicks route to the SAME curve machinery the inline - // Zone editor uses; anything else on the sheet is swallowed. - if (curvePopupOpen_) { - const CurvePopupLayout pl = computeCurvePopup(w, h); - if (contains(pl.close, x, y)) { - curvePopupOpen_ = false; - invalidate(); - return; - } - if (contains(pl.curveBox, x, y)) { - const int zi = ensureSampleZone(); - if (zi >= 0) handleCurveMouseDown(pl.curveBox, zi, x, y); - return; - } - if (popupOutsideSheet(pl, x, y) && drag_ == DragKind::kNone) { - curvePopupOpen_ = false; - invalidate(); - } - return; - } + // r11 curve popup: while open the sheet is modal — it owns every left-click. + if (handlePopupMouseDown(w, h, x, y)) return; const PerformanceZone probeZone = effectiveSampleZone(); const std::vector deckDescs = deckGroupDescs(probeZone.play); @@ -2459,7 +2378,10 @@ void ReaSamplerEditor::onMouseDown(int x, int y) { return; } - // ---- Zone surface (S-VIEW-8) ---- + // ---- Zone surface (S-VIEW-8 / FB2) ---- + // The curve popup is modal over the Zone surface too (FB2) — it owns every click while + // open, checked before every Zone affordance (incl. Back). + if (handlePopupMouseDown(w, h, x, y)) return; const Rect back{w - kPad - kNavButtonWidth, 2, w - kPad, (std::max)(2, (std::min)(kTitleHeight, h) - 2)}; if (contains(back, x, y)) { view_ = View::kSample; invalidate(); return; } @@ -2566,17 +2488,47 @@ void ReaSamplerEditor::onMouseDown(int x, int y) { } entryField_ = -1; // a click elsewhere in the Zone view cancels an in-progress entry - // The param panel: a toggle segment flips at once (commit); a slider grab starts a live drag. - // Only when a zone is selected (the Zone surface has no single-capture fallback — that lives - // on the Sample face now). The S-VIEW-10 curve box is checked first — it sits inside the - // panel's right column (zonesControlsArea is the panel minus that column). + // The per-zone param surface (FB2): the knob deck + the mini curve-preview button — the + // SAME grammar and hit-test machinery as the Sample face. Only when a zone is selected + // (the Zone surface has no single-capture fallback — that lives on the Sample face). if (selectedZone_ >= 0 && selectedZone_ < static_cast(map_.zones.size())) { - const Rect cb = zonesCurveBox(content); - if (cb.width() > 0 && contains(cb, x, y)) { - handleCurveMouseDown(cb, selectedZone_, x, y); + if (contains(zonesCurveButton(content), x, y)) { + curvePopupOpen_ = true; + invalidate(); return; } - handleControlClick(selectedZone_, zonesControlsArea(content), x, y); + const ZonePlaySeconds& play = map_.zones[static_cast(selectedZone_)].play; + const Rect deckArea = zonesDeckArea(content); + const DeckLayout dl = layoutDeck(zoneDeckGroupDescs(play), deckArea.left, deckArea.top, + deckArea.width()); + const DeckHit hit = hitTestDeck(dl, x, y); + if (hit.kind == DeckHitKind::CaptionToggle || hit.kind == DeckHitKind::RowToggle) { + // Zone-param toggles (play mode / pitch engine / pitch-env enable): a discrete, + // final edit committed at once (the deck precedent). No per-instance ids reach + // here — VOICE/MASTER are not in the zone group set. + applyZoneControl(selectedZone_, hit.id, 0.0, hit.segment); + commitAndReload(); + return; + } + if (hit.kind == DeckHitKind::Knob) { + // PITCH ENV knobs are Disabled (drawn, inert) while the envelope is off — the + // Sample deck's guard, mirrored. + const bool pitchEnvKnob = + hit.id == static_cast(ParamControl::kPitchEnvAttack) || + hit.id == static_cast(ParamControl::kPitchEnvDecay) || + hit.id == static_cast(ParamControl::kPitchEnvDepth); + if (pitchEnvKnob && !play.pitchEnv.enabled) return; + // GRAB-ANCHORED vertical drag (FA4): live-drag the map, commit on release. + drag_ = DragKind::kDeckKnob; + dragParamId_ = hit.id; + dragParamZone_ = selectedZone_; + dragStartMap_ = map_; + dragKnobStartValue_ = deckControlNorm( + hit.id, map_.zones[static_cast(selectedZone_)]); + dragStartX_ = x; + dragStartY_ = y; + invalidate(); + } } } @@ -2591,36 +2543,6 @@ void ReaSamplerEditor::applyZoneControl(int zoneIndex, int id, double value, int } } -bool ReaSamplerEditor::handleControlClick(int zoneIndex, const Rect& panel, int x, int y) { - if (zoneIndex < 0 || zoneIndex >= static_cast(map_.zones.size())) return false; - const ZonePlaySeconds& play = map_.zones[static_cast(zoneIndex)].play; - const std::vector descs = controlDescs(play); - const std::vector rows = layoutControls(panel, descs); - const int id = controlAtPoint(rows, x, y); - if (id < 0) return false; - for (const ControlRow& r : rows) { - if (r.id != id) continue; - if (r.kind == ControlKind::Toggle) { - const int seg = toggleSegmentHitTest(r.control, x, y); - if (seg >= 0) { - applyZoneControl(zoneIndex, id, 0.0, seg); - commitAndReload(); // a toggle is a discrete, final edit - } - } else { - // Grab the slider: set the value at the grab x immediately, then live-drag. - drag_ = DragKind::kParamSlider; - dragParamId_ = id; - dragParamZone_ = zoneIndex; - dragParamPanel_ = panel; - dragStartMap_ = map_; - applyZoneControl(zoneIndex, id, valueAtPoint(r.control, x), 0); - invalidate(); // live feedback; commit on WM_LBUTTONUP - } - break; - } - return true; -} - void ReaSamplerEditor::onMouseMove(int x, int y) { if (drag_ == DragKind::kNone) return; dragCurX_ = x; // keep the live cursor position for drag-state draw cues (e.g. drag-off warn) @@ -2773,25 +2695,6 @@ void ReaSamplerEditor::onMouseMove(int x, int y) { return; } - if (drag_ == DragKind::kParamSlider) { - // S12/S15/S16 + keyTrack (Zone surface only since r11 — the Sample face's controls are - // deck knobs): re-lay the panel and map x -> value against the grabbed control's live - // track rect. Uses dragParamZone_. - const int zi = dragParamZone_; - if (zi < 0 || zi >= static_cast(map_.zones.size())) return; - const ZonePlaySeconds& play = map_.zones[static_cast(zi)].play; - const std::vector descs = controlDescs(play); - const std::vector rows = layoutControls(dragParamPanel_, descs); - for (const ControlRow& r : rows) { - if (r.id == dragParamId_) { - applyZoneControl(zi, dragParamId_, valueAtPoint(r.control, x), 0); - break; - } - } - invalidate(); - return; - } - // Zone edits (kZoneLow/kZoneHigh/kZoneBody): recompute the grabbed field(s) live. Only reached // in the Zone surface where selectedZone_ is set + the strip lives under its content area. if (selectedZone_ < 0 || selectedZone_ >= static_cast(map_.zones.size())) return; @@ -2873,20 +2776,21 @@ void ReaSamplerEditor::onMouseRDown(int x, int y) { // r11 (issue 3c): right-click on a popup curve node deletes it — the PRIMARY delete // affordance; Alt-click and drag-off remain as landed alternates. Commits immediately // through the same path as Alt-click; deletePoint's endpoint guard makes an endpoint - // right-click a safe no-op. Right-clicks act ONLY while the popup is open (nothing else - // in the editor consumes them), and never during an in-flight left drag. - if (!processor_ || view_ != View::kSample || !curvePopupOpen_) return; + // right-click a safe no-op. Right-clicks act ONLY while the popup is open — over the + // Sample face OR the Zone surface (FB2; nothing else in the editor consumes them) — + // and never during an in-flight left drag. + if (!processor_ || view_ == View::kBrowse || !curvePopupOpen_) return; if (drag_ != DragKind::kNone) return; RECT rc{}; GetClientRect(childHwnd_, &rc); const CurvePopupLayout pl = computeCurvePopup(rc.right - rc.left, rc.bottom - rc.top); if (!contains(pl.curveBox, x, y)) return; - // Hit-test first (read-only, via effectiveSampleZone) so a right-click that lands between - // nodes does not materialize an uncommitted zone in map_. Materialize only on an actual hit. + // Hit-test first (read-only, via popupZone) so a right-click that lands between nodes + // does not materialize an uncommitted zone in map_. Materialize only on an actual hit. const VelocityCurve::Box box = curveBoxFromRect(pl.curveBox); - const int idx = effectiveSampleZone().velocityCurve.pointAtPixel(box, x, y); + const int idx = popupZone().velocityCurve.pointAtPixel(box, x, y); if (idx < 0) return; - const int zi = ensureSampleZone(); + const int zi = popupZoneIndex(); if (zi < 0) return; PerformanceZone& z = map_.zones[static_cast(zi)]; if (z.velocityCurve.deletePoint(static_cast(idx))) { @@ -2909,8 +2813,9 @@ void ReaSamplerEditor::onMouseWheel(int delta) { } void ReaSamplerEditor::onSearchChar(unsigned int ch) { - // r11 curve popup: Esc dismisses (checked first — the popup is modal over the Sample face, - // where neither the note-entry fields nor the Browse search can hold focus). + // r11 curve popup: Esc dismisses (checked first — the popup is modal over the Sample face + // or the Zone surface, FB2; opening it clears any note-entry focus, and the Browse search + // cannot hold focus under it). if (curvePopupOpen_ && ch == 27) { curvePopupOpen_ = false; invalidate(); diff --git a/src/vst/reasampler_editor.h b/src/vst/reasampler_editor.h index 862ecae..eb8a3c0 100644 --- a/src/vst/reasampler_editor.h +++ b/src/vst/reasampler_editor.h @@ -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& 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 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 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 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) ------