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:
@@ -326,11 +326,13 @@ anything for a trigger shape.
|
||||
|
||||
- `editor_geometry` (`core/instrument/ui`) — the shared geometry VOCABULARY every instrument UI module speaks: the `core::ui::Rect` alias, `contains()`, and `OverlayArea` (a one-field `Rect` wrapper, no implicit conversion from `Rect`). Header-only (an INTERFACE CMake target), so it carries no layout of its own.
|
||||
- `sample_bands` — **THE band-stack allocator**, and the only module that owns the Sample face's vertical inventory — including `kEditorMinWidth`/`kEditorMinHeight`, the editor's client-area floor, which IS its default size (the shell's `checkSizeConstraint` and opening `ViewRect` both read it; the face grows, never shrinks below what the stack is laid out for), and `kEditorCeilingWidth`, the floor's sibling window fact (the hard cap the floor may not exceed) — moved here from `knob_deck.h` since it is a window fact, not a deck one; the derivation identity against the deck's width budget stays in `test_deck_groups.cpp`, the one place that already includes both headers. Three bands top-to-bottom (CHROME toolbar+control row / WAVEFORM elastic, floored at two stacked lanes / DECKS bottom-anchored at the knob deck's own wrapped height), plus the waveform band's lane split (`waveformLanes` takes a resolved `LaneSplit`, not a raw bool — only `waveformSurface` folds the source-channel-count decision in). A shared READ-ONLY surface for every band owner — a band's interior module lays out inside the rect it is handed and never re-allocates the stack.
|
||||
- `sample_chrome` — the CHROME band's interior: the toolbar row (title + the whole right-anchored control run — bake Hold cell, bake, preview, velocity knob cell, channel toggle, Browse) over the strip row, which the piano strip owns outright. The title takes what the run leaves; the strip takes its whole row, inset only by the shared band pad so it lines up with the waveform band beneath. Every run member's width is RESERVED unconditionally, the Hold cell included — the only conditionally-drawn one, and the leftmost, so what its reservation buys is a title slot that does not re-measure when a loop is dialled in or out (`sample_chrome.h` records the cost). Also `previewGlyph`, the preview button's play triangle — three vertices for one filled-triangle draw, so the button's label needs no font metric and no image asset.
|
||||
- `sample_chrome` — the CHROME band's interior: the toolbar row (title + the whole right-anchored control run — bake Hold cell, bake, preview, velocity knob cell, loop enable, channel toggle, Browse) over the strip row, which the piano strip owns outright. The title takes what the run leaves; the strip takes its whole row, inset only by the shared band pad so it lines up with the waveform band beneath. Every run member's width is RESERVED unconditionally, the Hold cell included — the only conditionally-drawn one, and the leftmost, so what its reservation buys is a title slot that does not re-measure when a loop is dialled in or out (`sample_chrome.h` records the cost). Also `previewGlyph`, the preview button's play triangle — three vertices for one filled-triangle draw, so the button's label needs no font metric and no image asset.
|
||||
- `bake_hold` — the Hold knob's value domain and nothing else: the knob's normalized [0,1] mapped onto the note-length ladder and back, ordered by LENGTH rather than by the ladder's presentation order. Split from `sample_chrome` on the same axis `deck_values` was split from `knob_deck` — that says where the cell is, this says what its position means.
|
||||
- `keyboard_strip` — piano-keyboard strip: true white/black key geometry (whites tiled at one width, blacks overlaid at one width and height, straddling their boundary), hit-test resolving black-over-white by zone, root-marker rect, the absolute-position drag resolver, and MIDI note naming under the C4 convention. **Same-class keys are one integer width by construction; the residue of an indivisible band width (`w % 75`, up to 74 px) lands in symmetric end margins, never in a key** — uniform widths and gap-free edge-to-edge tiling cannot both hold, and uniformity wins.
|
||||
- `waveform_view` — the WAVEFORM band's interior: `waveformSurface` resolves the drawn lane(s) (two stacked lanes, L over R, only when the mode is stereo AND the source has a second channel — a mono source under stereo mode is dual-mono and draws one lane) plus **the** overlay area, and `laneEnvelope` splits one multi-channel envelope pass per lane. Also maps frame span linearly across a rect; generic named draggable markers with drag-delta resolver, clamp, and zero-crossing snap, plus `markerHandleRect` — a top-strip grab tab distinct from a marker's full-height column, so two markers that share a frame stay independently grabbable (the column goes to the first in draw order; the tab, asked first, resolves the other).
|
||||
- **Overlay contract (consumed by later waveform work).** `WaveformSurface::overlay` — equivalently the standalone `waveformOverlayArea(band)` — is the FULL band in both modes. Everything riding the waveform (the amp-envelope trace and its node handles, the start/loop markers, the loop region) draws ONCE into it, spanning both stacked lanes; hit-testing resolves against the same area so a grab in the lower lane reaches them. Anything drawn or hit-tested per lane is a duplicate and a defect — structurally enforced: `overlay` is the distinct `OverlayArea` type (`editor_geometry`), not `Rect`, so every overlay-consuming API (`frameToX`/`markerAtPoint`/`resolveDragFrame`, `envelope_edit`'s `nodeAtPoint`/`resolveNodeDrag`, `envelope_overlay`'s `buildEnvelopePolyline`) rejects a lane rect at compile time rather than silently accepting one.
|
||||
- **The four marks.** One grammar — line + shaped cap + label — over START / LOOP / END / XFADE. `markerHandleRect` IS the cap: every mark's is the same rect shape, only the glyph inside differs, which is what keeps the claim arbitration seeing one nominal cap area. `capAtPoint` resolves caps in the REVERSE of the column order, so any coincident PAIR stays separable (one answers its cap, the other its column) and the crossfade — the one mark with no column — can never be shadowed. `layoutMarkLabels` places the promoted (grabbed/hovered) mark first and suppresses any box that would overlap one already placed. `crossfadeWedgeHeight` is the ONE ramp both the audible region and the ingredient ghost draw, because they are the same fade weight over the two spans it mixes.
|
||||
- `loop_marks` — the loop enable's state machine, split from the geometry above on the axis the surface already has: that says where a mark is, this says what the loop IS. `SampleLoop::hasLoop` is the single authority and `resolveLoopMarks`/`applyLoopMarks` are its only two folds — the resolve re-parks on `defaultLoopBounds` only when the span is one `resolveLoop` would refuse (so a user's off keeps its positions and `parked` separates the two OFF states), and the write folds collapse-to-off in and ties the crossfade to the SPAN rather than to the enable. Links `loop_span` so the span the user is offered and the span the engine accepts stay one definition.
|
||||
- `capture_browser` — capture browser: card-grid layout + bank-filter tab strip geometry and hit-test; knows only counts and rects, draws nothing.
|
||||
- `browser_scroll` — scroll + type-to-filter layered over `capture_browser`: vertical scroll offset, scrollbar thumb, thumb-drag mapping, and name-substring search.
|
||||
- `param_taper` — THE norm↔value tapers every variable control shares, and the modifier vocabulary its drag surfaces read: the stage-time shifted-log (and `kStageTimeMaxSeconds`, the ONE home of the stage-time ceiling that `envelope_overlay`'s `kGateStageMaxSeconds` and `deck_values`' `kEnvTimeMaxSeconds` alias), the centre-expanded semitone-depth map, `DragModifiers`/`kFineDragScale`/`fineDrag`, the `UnitCategory` axis, and the four whole-unit snaps Shift applies. Extracted from `deck_values` because it has THREE consumers in two dependency layers — the knob's needle (`deck_values`), the AHDSR schematic axis and its drag inverse (`envelope_overlay`/`envelope_edit`, which sit *below* `deck_values`), and the VST3 host's `toPlain`/`toNormalized`. **Three functions that agree today is a defect, not an implementation choice**; solving the include edge by copying the map is the specific mistake this exists to prevent. Both maps resolve their output onto a fixed decimal quantum, which is what makes "every default has an EXACT normalized preimage" a structural guarantee rather than a libm coincidence — the header states the argument; the converse round trip at an arbitrary norm is explicitly NOT required.
|
||||
|
||||
@@ -29,6 +29,11 @@ reasampler_pure_library(waveform_view
|
||||
# waveform_view does not re-export.
|
||||
reasampler_test(waveform_view LINK waveform_view sample_bands)
|
||||
|
||||
# The loop enable's state machine. Links loop_span for the park bounds — the span the user is
|
||||
# offered and the span the engine accepts stay one definition.
|
||||
reasampler_pure_library(loop_marks SOURCES loop_marks.cpp LINK PUBLIC loop_span)
|
||||
reasampler_test(loop_marks LINK loop_marks)
|
||||
|
||||
reasampler_pure_library(browser_scroll
|
||||
SOURCES browser_scroll.cpp
|
||||
LINK PUBLIC capture_browser sample_chrome)
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// loop_marks.cpp — see loop_marks.h. Pure value folds; no host types.
|
||||
|
||||
#include "core/instrument/ui/loop_marks.h"
|
||||
|
||||
#include "core/instrument/engine/loop/loop_span.h" // defaultLoopBounds
|
||||
|
||||
namespace reasampler::instrument::ui {
|
||||
|
||||
namespace {
|
||||
|
||||
// The engine's own acceptance test, restated over the editor's two integers: resolveLoop
|
||||
// refuses an inverted, empty or out-of-range span rather than repairing it, so a span it would
|
||||
// refuse is one the handles must be re-parked out of.
|
||||
bool spanUsable(std::int64_t loopStart, std::int64_t loopEnd, std::int64_t frameCount) {
|
||||
return loopStart >= 0 && loopEnd > loopStart && loopEnd <= frameCount;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
LoopMarks resolveLoopMarks(const StoredLoop& stored, std::int64_t frameCount) {
|
||||
LoopMarks m;
|
||||
if (stored.override_) {
|
||||
m.hasLoop = stored.override_->hasLoop;
|
||||
m.loopStart = stored.override_->start;
|
||||
m.loopEnd = stored.override_->end;
|
||||
} else if (stored.intrinsic && stored.intrinsic->hasLoop) {
|
||||
m.hasLoop = true;
|
||||
m.loopStart = stored.intrinsic->start;
|
||||
m.loopEnd = stored.intrinsic->end;
|
||||
}
|
||||
if (stored.startPoint) m.start = *stored.startPoint;
|
||||
m.crossfade = stored.crossfade > 0 ? stored.crossfade : 0;
|
||||
|
||||
if (!spanUsable(m.loopStart, m.loopEnd, frameCount)) {
|
||||
m.hasLoop = false;
|
||||
m.parked = true;
|
||||
const engine::loop::LoopBounds d = engine::loop::defaultLoopBounds(frameCount);
|
||||
m.loopStart = d.start;
|
||||
m.loopEnd = d.end;
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
LoopWrite applyLoopMarks(const LoopMarks& m) {
|
||||
LoopWrite w;
|
||||
const bool spanAlive = m.loopEnd > m.loopStart;
|
||||
w.loop.hasLoop = m.hasLoop && spanAlive;
|
||||
w.loop.start = m.loopStart;
|
||||
w.loop.end = m.loopEnd;
|
||||
w.crossfade = (spanAlive && m.crossfade > 0) ? m.crossfade : 0;
|
||||
w.start = m.start;
|
||||
return w;
|
||||
}
|
||||
|
||||
} // namespace reasampler::instrument::ui
|
||||
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
// loop_marks.h — the loop enable's state machine: what the waveform band SHOWS for a stored
|
||||
// loop, and what a marker edit WRITES back. `SampleLoop::hasLoop` is the single authority;
|
||||
// collapse-to-off and drag-to-create are shortcuts onto it, not a second state. Pure values
|
||||
// only — the shell supplies the stored side and applies the result.
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
#include "core/instrument/engine/play_params.h" // SampleLoop
|
||||
|
||||
namespace reasampler::instrument::ui {
|
||||
|
||||
using ::reasampler::SampleLoop;
|
||||
|
||||
// What the four marks are showing. `parked` separates the two OFF states: a pair sitting on
|
||||
// defaultLoopBounds because nothing was ever set (there is a loop to "set") from a real span
|
||||
// the user switched off (there is not).
|
||||
struct LoopMarks {
|
||||
std::int64_t start = 0;
|
||||
std::int64_t loopStart = 0;
|
||||
std::int64_t loopEnd = 0;
|
||||
std::int64_t crossfade = 0; // pre-seam fade, SOURCE frames
|
||||
bool hasLoop = false;
|
||||
bool parked = false;
|
||||
};
|
||||
|
||||
// The stored side: the parameter set's loop override, the bank's loop intrinsic (consulted only
|
||||
// when there is no override — the override always supersedes it), the crossfade length, and the
|
||||
// start point.
|
||||
struct StoredLoop {
|
||||
std::optional<SampleLoop> override_;
|
||||
std::optional<SampleLoop> intrinsic;
|
||||
std::int64_t crossfade = 0;
|
||||
std::optional<std::int64_t> startPoint;
|
||||
};
|
||||
|
||||
// Reads the stored loop into what the band shows. A span the engine could not honour —
|
||||
// collapsed, inverted, or outside [0, frameCount] — re-parks on defaultLoopBounds so two
|
||||
// coincident handles can never become ungrabbable; a VALID span keeps its own positions
|
||||
// whatever the enable says, which is what makes the enable a toggle rather than a delete
|
||||
// button.
|
||||
LoopMarks resolveLoopMarks(const StoredLoop& stored, std::int64_t frameCount);
|
||||
|
||||
// The write side, the inverse of resolveLoopMarks.
|
||||
struct LoopWrite {
|
||||
SampleLoop loop;
|
||||
std::int64_t crossfade = 0;
|
||||
std::int64_t start = 0;
|
||||
};
|
||||
|
||||
// Folds collapse-to-off in: a span dragged onto itself is the OFF gesture, recorded as such so
|
||||
// the next resolve re-offers the default handles. The crossfade goes with the SPAN, not with
|
||||
// the enable — zeroed only when the span is destroyed. That preserves the original zeroing
|
||||
// rule's reason rather than overruling it: a stale length could silently re-apply against a
|
||||
// span that no longer exists, but a retained span retains its clamp bound too, so nothing is
|
||||
// stale.
|
||||
LoopWrite applyLoopMarks(const LoopMarks& m);
|
||||
|
||||
} // namespace reasampler::instrument::ui
|
||||
@@ -19,6 +19,10 @@ constexpr int kStripBandHeight = 30;
|
||||
constexpr int kRunGap = 6; // between adjacent items of the toolbar run
|
||||
constexpr int kChanSegW = 52;
|
||||
constexpr int kChanSegH = 18;
|
||||
// The loop enable's segments carry a two-word label, so they are wider than Mono|Stereo's.
|
||||
// If the title slot ever fails to hold its text at the editor's floor, THIS narrows — the
|
||||
// floor does not move.
|
||||
constexpr int kLoopSegW = 58;
|
||||
constexpr int kVelCellW = 56;
|
||||
constexpr int kHoldCellW = 56; // the bake Hold cell, same grammar as the velocity cell
|
||||
constexpr int kVelLabelH = 16;
|
||||
@@ -43,7 +47,8 @@ ChromeRects chromeRects(const Rect& chrome, int knobSize) {
|
||||
const auto topFor = [&row](int h) { return row.y + (row.height - h) / 2; };
|
||||
const auto leftOf = [&row](int edge, int w) { return std::max(row.x, edge - w); };
|
||||
|
||||
// The fixed run, right to left: Browse, Mono|Stereo, velocity cell, preview, bake, hold.
|
||||
// The fixed run, right to left: Browse, Mono|Stereo, Loop Off|On, velocity cell, preview,
|
||||
// bake, hold.
|
||||
// The velocity-curve button that used to sit here now lives in the deck's VELOCITY group.
|
||||
const int navH = std::min(kRunButtonH, row.height);
|
||||
const int navTop = topFor(navH);
|
||||
@@ -58,9 +63,15 @@ ChromeRects chromeRects(const Rect& chrome, int knobSize) {
|
||||
r.chanMono = Rect::ltrb(leftOf(r.chanStereo.x, kChanSegW), chanTop, r.chanStereo.x,
|
||||
chanTop + kChanSegH);
|
||||
|
||||
const int loopRight = leftOf(r.chanMono.x, kRunGap);
|
||||
r.loopOn = Rect::ltrb(leftOf(loopRight, kLoopSegW), chanTop, loopRight,
|
||||
chanTop + kChanSegH);
|
||||
r.loopOff = Rect::ltrb(leftOf(r.loopOn.x, kLoopSegW), chanTop, r.loopOn.x,
|
||||
chanTop + kChanSegH);
|
||||
|
||||
const int cellH = std::min(row.height, knobSize + kVelLabelH);
|
||||
const int cellTop = topFor(cellH);
|
||||
const int cellRight = leftOf(r.chanMono.x, kRunGap);
|
||||
const int cellRight = leftOf(r.loopOff.x, kRunGap);
|
||||
r.velCell = Rect::ltrb(leftOf(cellRight, kVelCellW), cellTop, cellRight, cellTop + cellH);
|
||||
const int knobLeft = r.velCell.x + (r.velCell.width - knobSize) / 2;
|
||||
r.velKnob = Rect::ltrb(knobLeft, r.velCell.y, knobLeft + knobSize,
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
// sample_chrome.h — interior geometry of the Sample face's CHROME band: the toolbar row
|
||||
// (title + the whole right-anchored control run + Browse) over the strip row, which the
|
||||
// piano strip has to itself. Reads the band rect the allocator hands it (sample_bands) and
|
||||
// never allocates vertical space of its own.
|
||||
// never allocates vertical space of its own. The run is right-anchored and the title takes
|
||||
// the remainder, so a member added to the run costs the title, never the window's floor.
|
||||
|
||||
#include "core/instrument/ui/editor_geometry.h" // Rect
|
||||
|
||||
@@ -32,6 +33,11 @@ struct ChromeRects {
|
||||
Rect velCell; // preview-velocity knob cell (knob + label band)
|
||||
Rect velKnob;
|
||||
Rect velLabel;
|
||||
// The sustain loop's enable. Immediately left of the channel toggle because it is the same
|
||||
// class of control — a playback mode of the loaded capture — and because the run is
|
||||
// right-anchored, so the title slot absorbs its width and the editor's floor does not move.
|
||||
Rect loopOff;
|
||||
Rect loopOn;
|
||||
Rect chanMono;
|
||||
Rect chanStereo;
|
||||
Rect navBrowse;
|
||||
|
||||
@@ -65,14 +65,16 @@ struct WaveformClaim {
|
||||
enum class WaveformClaimant { kNone, kNode, kTab, kMarker };
|
||||
|
||||
// The overlay's cross-affordance arbitration: a contour node (or, mutually exclusively, a
|
||||
// staged envelope's drag node — both feed the same `node` slot), the loop crossfade tab, and a
|
||||
// marker's full-height column can all claim the same pixel. Hit gates a candidate out
|
||||
// entirely; among the ones that hit, the SMALLEST nominal area wins — the marker column is the
|
||||
// odd one out (its target is the whole overlay height), so it only wins where nothing narrower
|
||||
// also claims the click. Ties go to whichever is checked first: node, then tab, then marker —
|
||||
// no live geometry produces a tie except tab-vs-marker, which the tab correctly wins (see
|
||||
// editor_input_waveform.cpp's mouseDownWaveform for the live constants). A control-click has no
|
||||
// tab/marker meaning (they answer plain grabs only), so it resolves to the node whenever the
|
||||
// staged envelope's drag node — both feed the same `node` slot), a mark's CAP, and a mark's
|
||||
// full-height column can all claim the same pixel. Hit gates a candidate out entirely; among
|
||||
// the ones that hit, the SMALLEST nominal area wins — the column is the odd one out (its target
|
||||
// is the whole overlay height), so it only wins where nothing narrower also claims the click.
|
||||
// Ties go to whichever is checked first: node, then tab, then marker — no live geometry
|
||||
// produces a tie except tab-vs-marker, which the tab correctly wins (see
|
||||
// editor_input_waveform.cpp's mouseDownWaveform for the live constants). Every mark's cap is
|
||||
// one markerHandleRect, so the `tab` slot carries ONE nominal area however many marks feed it;
|
||||
// which mark it resolves to is waveform_view's capAtPoint, not this. A control-click has no
|
||||
// cap/column meaning (they answer plain grabs only), so it resolves to the node whenever the
|
||||
// node is in the running, regardless of area.
|
||||
WaveformClaimant resolveWaveformClaim(const WaveformClaim& node, const WaveformClaim& tab,
|
||||
const WaveformClaim& marker, SplineGesture gesture);
|
||||
|
||||
@@ -79,6 +79,66 @@ Rect markerHandleRect(const OverlayArea& area, std::int64_t frameCount, std::int
|
||||
return Rect{left, r.y, right - left, std::min(kMarkerHandleHeight, r.height)};
|
||||
}
|
||||
|
||||
int capAtPoint(const OverlayArea& area, std::int64_t frameCount, const WaveMarks& marks, int x,
|
||||
int y) {
|
||||
for (int i = kWaveMarkCount - 1; i >= 0; --i) {
|
||||
if (!marks.present[i]) continue;
|
||||
if (contains(markerHandleRect(area, frameCount, marks.frame[i]), x, y)) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool markLabelLeftOfLine(WaveMark m) {
|
||||
return m == WaveMark::kLoopEnd || m == WaveMark::kCrossfade;
|
||||
}
|
||||
|
||||
Rect markLabelRect(const OverlayArea& area, std::int64_t frameCount, std::int64_t frame,
|
||||
bool leftOfLine, int textWidth) {
|
||||
const Rect& r = area.rect;
|
||||
if (r.empty() || textWidth <= 0 || textWidth > r.width) return Rect{};
|
||||
const int h = std::min(kMarkLabelHeight, r.height - kMarkerHandleHeight);
|
||||
if (h <= 0) return Rect{};
|
||||
const int mx = frameToX(area, frameCount, frame);
|
||||
int left = leftOfLine ? mx - kMarkLabelGap - textWidth : mx + kMarkLabelGap;
|
||||
left = std::max(r.x, std::min(left, r.right() - textWidth));
|
||||
return Rect{left, r.y + kMarkerHandleHeight, textWidth, h};
|
||||
}
|
||||
|
||||
WaveMarkLabels layoutMarkLabels(const OverlayArea& area, std::int64_t frameCount,
|
||||
const WaveMarks& marks, const int* textWidth, int promoted) {
|
||||
WaveMarkLabels out;
|
||||
if (textWidth == nullptr) return out;
|
||||
// The promoted mark first, then draw order. Every label shares one row, so "would overlap
|
||||
// one already placed" reduces to a horizontal span test.
|
||||
int order[kWaveMarkCount + 1] = {promoted, 0, 1, 2, 3};
|
||||
for (int slot = 0; slot < kWaveMarkCount + 1; ++slot) {
|
||||
const int i = order[slot];
|
||||
if (i < 0 || i >= kWaveMarkCount) continue;
|
||||
if (!marks.present[i] || !out.box[i].empty()) continue;
|
||||
const Rect box = markLabelRect(area, frameCount, marks.frame[i],
|
||||
markLabelLeftOfLine(static_cast<WaveMark>(i)),
|
||||
textWidth[i]);
|
||||
if (box.empty()) continue;
|
||||
bool clash = false;
|
||||
for (int j = 0; j < kWaveMarkCount && !clash; ++j) {
|
||||
clash = !out.box[j].empty() && box.x < out.box[j].right() &&
|
||||
out.box[j].x < box.right();
|
||||
}
|
||||
if (!clash) out.box[i] = box;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
int crossfadeWedgeHeight(int x0, int x1, int x) {
|
||||
const int w = x1 - x0;
|
||||
if (w <= 0 || x < x0 || x >= x1) return 0;
|
||||
if (w == 1) return kCrossfadeWedgePx;
|
||||
// Normalized over w - 1 so the LAST drawn column lands exactly on the peak, the same
|
||||
// reason crossfadeWeight normalizes over crossfade - 1 (loop_span.h).
|
||||
const int d = x - x0;
|
||||
return (kCrossfadeWedgePx * d + (w - 1) / 2) / (w - 1);
|
||||
}
|
||||
|
||||
int markerAtPoint(const OverlayArea& area, std::int64_t frameCount, const std::int64_t* frames,
|
||||
int count, int x, int y) {
|
||||
if (count <= 0 || frames == nullptr) return -1;
|
||||
|
||||
@@ -65,17 +65,75 @@ int frameToX(const OverlayArea& area, std::int64_t frameCount, std::int64_t fram
|
||||
// area.x yields 0; right of area.right() yields frameCount.
|
||||
std::int64_t xToFrame(const OverlayArea& area, std::int64_t frameCount, int x);
|
||||
|
||||
// A marker's grab HANDLE: a tab riding the top of the overlay, centred on the marker's x and
|
||||
// clipped into the area. Distinct from the full-height grab COLUMN markerAtPoint answers, so
|
||||
// two markers that share a frame stay independently grabbable — the handle owns the top
|
||||
// strip, the column owns everything below it. Without that split, first-in-draw-order wins
|
||||
// every coincident tie and the loser can never be dragged apart again.
|
||||
// A marker's grab HANDLE — THE CAP, in the mark grammar's vocabulary: a tab riding the top of
|
||||
// the overlay, centred on the marker's x and clipped into the area. Distinct from the
|
||||
// full-height grab COLUMN markerAtPoint answers, so two markers that share a frame stay
|
||||
// independently grabbable — the handle owns the top strip, the column owns everything below
|
||||
// it. Without that split, first-in-draw-order wins every coincident tie and the loser can never
|
||||
// be dragged apart again. Every mark's cap is this ONE rect shape; only the glyph drawn inside
|
||||
// it differs, which is what lets the claim arbitration see a single nominal cap area.
|
||||
inline constexpr int kMarkerHandleHeight = 10;
|
||||
// Same half-width as the column's own grab band on purpose: the handle is that same grab
|
||||
// tolerance, just confined to the top strip, not an independent tuning.
|
||||
inline constexpr int kMarkerHandleHalfWidth = kMarkerGrabWidth;
|
||||
Rect markerHandleRect(const OverlayArea& area, std::int64_t frameCount, std::int64_t frame);
|
||||
|
||||
// --- The overlay's four marks -------------------------------------------------------------
|
||||
|
||||
// The marks the overlay carries, in DRAW and COLUMN-hit order. The shell's WaveMarker aliases
|
||||
// this, so the drag router and the geometry below cannot disagree about an ordinal.
|
||||
enum class WaveMark { kStart = 0, kLoopStart = 1, kLoopEnd = 2, kCrossfade = 3, kCount = 4 };
|
||||
inline constexpr int kWaveMarkCount = static_cast<int>(WaveMark::kCount);
|
||||
|
||||
// Which marks are on screen and where. A mark that is not `present` is excluded from every
|
||||
// answer below — with no loop set there is no crossfade mark to reach.
|
||||
struct WaveMarks {
|
||||
std::int64_t frame[kWaveMarkCount] = {0, 0, 0, 0};
|
||||
bool present[kWaveMarkCount] = {false, false, false, false};
|
||||
};
|
||||
|
||||
// Which mark's cap a point lands on, or -1. Caps resolve in the REVERSE of the column order
|
||||
// markerAtPoint uses — crossfade, end, loop start, start — and that reversal is the whole
|
||||
// separability argument: whichever mark of a coincident PAIR loses the cap still answers its
|
||||
// own full-height column, and the crossfade, the one mark with no column at all, is first so
|
||||
// nothing can shadow it. A coincident TRIPLE still strands its middle mark, exactly as the
|
||||
// pre-cap tab/column split did.
|
||||
int capAtPoint(const OverlayArea& area, std::int64_t frameCount, const WaveMarks& marks,
|
||||
int x, int y);
|
||||
|
||||
// Labels sit in the row directly below the caps, beside the mark's line: START and LOOP to the
|
||||
// right of it, END and XFADE to the left, so a label never crosses into the span it bounds.
|
||||
inline constexpr int kMarkLabelHeight = 10;
|
||||
inline constexpr int kMarkLabelGap = 3; // between the mark's line and its text
|
||||
|
||||
bool markLabelLeftOfLine(WaveMark m);
|
||||
|
||||
// The box `textWidth` px of label occupies for a mark at `frame`. Nudged inside the area rather
|
||||
// than clipped — half a label reads as a different mark's — and empty when it cannot fit.
|
||||
Rect markLabelRect(const OverlayArea& area, std::int64_t frameCount, std::int64_t frame,
|
||||
bool leftOfLine, int textWidth);
|
||||
|
||||
// The placed labels; an empty box is a label that is not drawn. `textWidth` is per mark, in the
|
||||
// caller's own font (this module measures no text). `promoted` — a WaveMark ordinal, or -1 —
|
||||
// is placed FIRST and so can never be the one suppressed: it is the mark the user is grabbing
|
||||
// or hovering, i.e. the one they are asking about.
|
||||
struct WaveMarkLabels {
|
||||
Rect box[kWaveMarkCount];
|
||||
};
|
||||
WaveMarkLabels layoutMarkLabels(const OverlayArea& area, std::int64_t frameCount,
|
||||
const WaveMarks& marks, const int* textWidth, int promoted);
|
||||
|
||||
// The crossfade region's peak edge-wedge height. The region draws as a wedge at the overlay's
|
||||
// top and bottom edges and NEVER as a second fill: it now sits INSIDE the loop span, where a
|
||||
// translucent fill would stack on the loop fill over an already-accepted under-floor contrast
|
||||
// pair (see editor_paint_waveform.cpp).
|
||||
inline constexpr int kCrossfadeWedgePx = 10;
|
||||
|
||||
// Wedge height at pixel column `x` over [x0, x1): zero at x0, kCrossfadeWedgePx at x1 - 1. The
|
||||
// audible region and the ingredient ghost are the SAME ramp over the two spans the fade mixes,
|
||||
// so one function draws both.
|
||||
int crossfadeWedgeHeight(int x0, int x1, int x);
|
||||
|
||||
// Which marker (index into the caller's parallel `frames` array, in draw order) a grab at
|
||||
// (x, y) lands on, or -1 for a miss. A marker is grabbed when x is within kMarkerGrabWidth of
|
||||
// its drawn x and y is inside `area`. First marker in draw order wins an overlapping tie.
|
||||
|
||||
Reference in New Issue
Block a user