Loop: an explicit enable, four named marks with grabbable caps, and the crossfade painted where it is actually heard
hasLoop becomes user-owned with the gestures as shortcuts onto it; no format change. START uses overlay/trace, not accent/primary, which is the waveform's own fill.
This commit is contained in:
@@ -55,16 +55,20 @@ bool ReaSamplerEditor::mouseDownWaveform(const FaceLayout& fl, int x, int y) {
|
||||
}
|
||||
|
||||
const SetupMarkers m = pickedMarkers(frames);
|
||||
const WaveMarks grabbable = grabbableMarks(m);
|
||||
const std::int64_t markerFrames[3] = {m.start, m.loopStart, m.loopEnd};
|
||||
|
||||
// Three affordances can claim the same pixel: a node (the staged envelope's or the drawn
|
||||
// contour's — a small fixed pick box either way), the crossfade tab (a small clipped
|
||||
// top-strip tab), and a marker's full-height grab column (waveform_view.h's tab-vs-column
|
||||
// split already keeps the tab apart from ITS OWN column; this is the cross-affordance case
|
||||
// on top of that). resolveWaveformClaim (spline_edit.h) is the ONE arbitration: it measures
|
||||
// each claimant's own NOMINAL target area and lets the smallest hit win, since a fixed check
|
||||
// order shadows whichever one loses the tie — this seam regressed twice from exactly that
|
||||
// fix. Never add here (kAdd is only tried once nothing else has claimed the click, below).
|
||||
// contour's — a small fixed pick box either way), a mark's CAP (a small clipped top-strip
|
||||
// tab), and a mark's full-height grab column (waveform_view.h's cap-vs-column split already
|
||||
// keeps a cap apart from ITS OWN column; this is the cross-affordance case on top of that).
|
||||
// resolveWaveformClaim (spline_edit.h) is the ONE arbitration: it measures each claimant's
|
||||
// own NOMINAL target area and lets the smallest hit win, since a fixed check order shadows
|
||||
// whichever one loses the tie — this seam regressed twice from exactly that fix. Giving
|
||||
// every mark a cap changed WHICH mark the cap slot resolves to, not the slot's nominal area
|
||||
// (every cap is one markerHandleRect) and not the ordering cap < node < column, so the
|
||||
// arbitration itself is unchanged. Never add here (kAdd is only tried once nothing else has
|
||||
// claimed the click, below).
|
||||
WaveformClaim node;
|
||||
if (envNodeHit.hit) {
|
||||
constexpr std::int64_t side = 2 * kNodeGrabRadius + 1;
|
||||
@@ -81,18 +85,22 @@ bool ReaSamplerEditor::mouseDownWaveform(const FaceLayout& fl, int x, int y) {
|
||||
}
|
||||
}
|
||||
|
||||
const Rect tabRect =
|
||||
m.hasLoop ? markerHandleRect(overlay, frames, m.loopStart - m.crossfade) : Rect{};
|
||||
const WaveformClaim tab = (m.hasLoop && contains(tabRect, x, y))
|
||||
? WaveformClaim{true, static_cast<std::int64_t>(tabRect.width) *
|
||||
tabRect.height}
|
||||
: WaveformClaim{};
|
||||
const int capHit = capAtPoint(overlay, frames, grabbable, x, y);
|
||||
const WaveformClaim tab =
|
||||
(capHit >= 0)
|
||||
? WaveformClaim{true, static_cast<std::int64_t>(2 * kMarkerHandleHalfWidth + 1) *
|
||||
kMarkerHandleHeight}
|
||||
: WaveformClaim{};
|
||||
|
||||
// Nominal, not actual: markerAtPoint clips the column at the overlay edges (a marker at
|
||||
// frame 0 has 6 usable columns, not 11) and the node's fixed side clips too at a pick-box
|
||||
// corner. Both overestimate in the direction that already produces the intended winner, so
|
||||
// the arbitration runs on NOMINAL area, not the measured hit-testable pixel count.
|
||||
const int markerHit = markerAtPoint(overlay, frames, markerFrames, 3, x, y);
|
||||
const int markerHit =
|
||||
(grabbable.present[static_cast<int>(WaveMark::kLoopStart)]
|
||||
? markerAtPoint(overlay, frames, markerFrames, 3, x, y)
|
||||
// In Trigger only START answers a column, and it is index 0 of the same array.
|
||||
: markerAtPoint(overlay, frames, markerFrames, 1, x, y));
|
||||
const WaveformClaim marker =
|
||||
(markerHit >= 0)
|
||||
? WaveformClaim{true, static_cast<std::int64_t>(2 * kMarkerGrabWidth + 1) *
|
||||
@@ -114,7 +122,7 @@ bool ReaSamplerEditor::mouseDownWaveform(const FaceLayout& fl, int x, int y) {
|
||||
}
|
||||
return splineOverlayClick(overlay, x, y, gesture, /*addOnEmptySpace=*/false);
|
||||
case WaveformClaimant::kTab:
|
||||
beginMarkerDrag(WaveMarker::kLoopXfade, m, frames, x);
|
||||
beginMarkerDrag(static_cast<WaveMarker>(capHit), m, frames, x);
|
||||
return true;
|
||||
case WaveformClaimant::kMarker:
|
||||
beginMarkerDrag(static_cast<WaveMarker>(markerHit), m, frames, x);
|
||||
@@ -179,6 +187,28 @@ bool ReaSamplerEditor::splineOverlayClick(const OverlayArea& waveArea, int x, in
|
||||
return true;
|
||||
}
|
||||
|
||||
ReaSamplerEditor::HoverTarget ReaSamplerEditor::hoverWaveform(const FaceLayout& fl, int x,
|
||||
int y) {
|
||||
// Caps only: the cap is the grip, so it is the one thing on the overlay a resting pointer
|
||||
// can be "on". A hovered mark promotes its own label past the suppression rule.
|
||||
//
|
||||
// Rejected on the band's own rect FIRST, before anything expensive: this runs on every
|
||||
// WM_MOUSEMOVE, and with no loop override set pickedMarkers costs a bridge read plus a bank
|
||||
// parse. Every cap lives in the top kMarkerHandleHeight of the band, so that strip is the
|
||||
// only place the answer can be anything but a miss.
|
||||
const Rect& band = fl.bands.waveform;
|
||||
if (band.empty() || x < band.x || x >= band.right() || y < band.y ||
|
||||
y >= band.y + kMarkerHandleHeight) {
|
||||
return {};
|
||||
}
|
||||
const auto frames = static_cast<std::int64_t>(monoPcmFor(selectedId_).size());
|
||||
if (frames <= 0) return {};
|
||||
const OverlayArea overlay = waveformOverlayArea(band);
|
||||
const int cap = capAtPoint(overlay, frames, grabbableMarks(pickedMarkers(frames)), x, y);
|
||||
if (cap < 0) return {};
|
||||
return {HoverKind::kWaveMark, cap};
|
||||
}
|
||||
|
||||
void ReaSamplerEditor::beginMarkerDrag(WaveMarker which, const SetupMarkers& m,
|
||||
std::int64_t frames, int x) {
|
||||
drag_ = DragKind::kWaveMarker;
|
||||
@@ -243,7 +273,7 @@ void ReaSamplerEditor::dragWaveform(const FaceLayout& fl, int x, int y) {
|
||||
const int idx = static_cast<int>(waveMarker_);
|
||||
const std::int64_t startVals[4] = {dragStartMarkers_.start, dragStartMarkers_.loopStart,
|
||||
dragStartMarkers_.loopEnd,
|
||||
dragStartMarkers_.loopStart -
|
||||
dragStartMarkers_.loopEnd -
|
||||
dragStartMarkers_.crossfade};
|
||||
std::int64_t newFrame = resolveDragFrame(overlay, frames, startVals[idx], dx);
|
||||
|
||||
@@ -251,13 +281,16 @@ void ReaSamplerEditor::dragWaveform(const FaceLayout& fl, int x, int y) {
|
||||
// frames — no host types, no file I/O. The crossfade handle is exempt: it sets a fade
|
||||
// LENGTH, and the whole point of the fade is that its edges need no zero crossing.
|
||||
const std::vector<AudioSample>& pcm = monoPcmFor(selectedId_);
|
||||
if (!pcm.empty() && waveMarker_ != WaveMarker::kLoopXfade) {
|
||||
if (!pcm.empty() && waveMarker_ != WaveMarker::kCrossfade) {
|
||||
newFrame = nearestZeroCrossing(pcm.data(), static_cast<std::int64_t>(pcm.size()),
|
||||
newFrame);
|
||||
}
|
||||
|
||||
// Build the edited marker set from the snapshot, moving only the grabbed marker, then
|
||||
// clamp: loopStart <= loopEnd, start in [0, frames-1]. Dragging a loop marker MAKES a loop.
|
||||
// clamp: loopStart <= loopEnd, start in [0, frames-1]. Dragging a LOOP marker turns the
|
||||
// enable on — a grab implies intent to loop, and it is what teaches the chrome toggle by
|
||||
// demonstration. Dragging START does not: it is live in both modes and says nothing about
|
||||
// the loop.
|
||||
SetupMarkers m = dragStartMarkers_;
|
||||
if (waveMarker_ == WaveMarker::kStart) {
|
||||
m.start = newFrame;
|
||||
@@ -267,8 +300,8 @@ void ReaSamplerEditor::dragWaveform(const FaceLayout& fl, int x, int y) {
|
||||
} else if (waveMarker_ == WaveMarker::kLoopEnd) {
|
||||
m.loopEnd = (std::max)(newFrame, m.loopStart);
|
||||
m.hasLoop = true;
|
||||
} else { // kLoopXfade — the handle sits at loopStart - crossfade, so left lengthens it
|
||||
m.crossfade = (std::max)(std::int64_t{0}, m.loopStart - newFrame);
|
||||
} else { // kCrossfade — the handle sits at loopEnd - crossfade, so left still lengthens it
|
||||
m.crossfade = (std::max)(std::int64_t{0}, m.loopEnd - newFrame);
|
||||
}
|
||||
if (m.start < 0) m.start = 0;
|
||||
if (m.start > frames - 1) m.start = frames - 1;
|
||||
|
||||
Reference in New Issue
Block a user