fix: restore waveform symmetry about the midline, cut deck_values' link to the bank model, and unit-test the column arithmetic

The waveform column's vertical extents move to pure component_geometry so the shared
primitive stops being untested; PlaySeconds hoists into a header-only play_seconds target.
This commit is contained in:
2026-08-01 09:47:07 -04:00
parent 7f74b11dce
commit ca464397b2
17 changed files with 310 additions and 138 deletions
+4 -4
View File
@@ -7,6 +7,7 @@
#ifdef _WIN32
#include "core/instrument/ui/keyboard_strip.h" // keyAtPoint / resolveDragNote (root key)
#include "core/instrument/ui/knob_deck.h" // inKnobFace (the shared reset target rule)
#include "shell/instrument/editor_internal.h"
#include "shell/instrument/reasampler_processor.h"
@@ -82,11 +83,10 @@ bool ReaSamplerEditor::mouseDownChrome(const FaceLayout& fl, int x, int y) {
bool ReaSamplerEditor::doubleClickChrome(const FaceLayout& fl, int x, int y) {
// The preview-velocity dial answers the same reset gesture the deck's knobs do — it is a
// radial knob drawn by the same primitive, so a user who learns the gesture there expects
// it here. Resolved against the whole cell, not the circle: the cell IS the knob's target
// on this surface (there is no neighbouring control to steal from).
// radial knob drawn by the same primitive, so it resolves through the same target rule
// (inKnobFace), against the drawn circle rather than the cell's label band.
if (selectedId_.empty() || !processor_) return false;
if (!contains(fl.chrome.velCell, x, y)) return false;
if (!inKnobFace(fl.chrome.velKnob, x, y)) return false;
processor_->setPreviewVelocity(kPreviewVelocityDefault);
invalidate();
return true;
+1 -1
View File
@@ -56,7 +56,7 @@ live in `shell/bank_ops`, a sibling directory, not here.
- `panel_thumbnails` — the PCM→envelope thumbnail cache + the bank-change fingerprint pass.
- `panel_audition` — the preview-playback engine.
- `panel_bank_ops` — the menu/prompt UX skin over the promptless `shell/bank_ops` verbs.
- `draw_kit` — shared LICE draw shell: `fillSurface`, `drawButton`/`drawSlider`/`drawListRow`/`drawWaveform`, cached-font `text()`, full interaction-state model, double-buffer preserved. Consumes `theme` + `component_geometry`.
- `draw_kit` — shared LICE draw shell: `fillSurface`, `drawButton`/`drawSlider`/`drawListRow`/`drawWaveform`, cached-font `text()`, full interaction-state model, double-buffer preserved. Consumes `theme` + `component_geometry`. Its antialiasing contract — which surfaces alias, which cannot, and the LICE primitive each answers with — is the disposition table in `docs/product/visual-design-language.md` §8, which governs `drawWaveform` as much as this doc does. `drawWaveform`'s per-column vertical arithmetic is NOT here: it is the pure, unit-tested `component_geometry::waveformBand`/`waveformColumnSpan` pair, because this TU is DAW-verified and not unit-tested.
## Gotchas
+15 -19
View File
@@ -27,6 +27,10 @@ using ui::compressAmplitudeForDisplay;
using ui::roleColor;
using ui::roleColorState;
using ui::spectralColor;
using ui::WaveformBand;
using ui::waveformBand;
using ui::WaveformColumnSpan;
using ui::waveformColumnSpan;
// The one place a pure KitColor becomes a LICE_pixel (LICE_RGBA(r,g,b,a), verified against
// lice.h). The theme owns the color; the shell owns the packing.
@@ -289,11 +293,9 @@ void drawWaveform(LICE_IBitmap* bmp, const KitBox& box, const Envelope& env) {
for (int ch = 0; ch < channels; ++ch) {
const ChannelEnvelope& bins = env[static_cast<std::size_t>(ch)];
const int bandTop = box.y + ch * bandH;
const int midY = bandTop + bandH / 2;
const double halfSpan = (bandH / 2) - 2;
const WaveformBand band = waveformBand(box.y + ch * bandH, bandH);
LICE_Line(bmp, box.x + 2, midY, box.x + box.width - 2, midY,
LICE_Line(bmp, box.x + 2, band.midY, box.x + box.width - 2, band.midY,
midCol, 1.0f, 0, false);
if (bins.empty() || innerW <= 0) continue;
@@ -303,28 +305,22 @@ void drawWaveform(LICE_IBitmap* bmp, const KitBox& box, const Envelope& env) {
// alone leaves the outline stepped — a vertical span has no aa to apply — and where two
// adjacent columns differ sharply it reads as a comb rather than one envelope. The
// stroke is the SAME ink as the fill it edges, so it can only soften the boundary.
double prevTop = 0.0;
double prevBottom = 0.0;
WaveformColumnSpan prev;
for (int col = 0; col < innerW; ++col) {
const MinMax mm = columnMinMax(bins, innerW, col);
const int x = box.x + 2 + col;
const double topF = midY - compressAmplitudeForDisplay(mm.max) * halfSpan;
const double bottomF = midY - compressAmplitudeForDisplay(mm.min) * halfSpan;
int yMax = static_cast<int>(topF);
int yMin = static_cast<int>(bottomF);
if (yMax < bandTop) yMax = bandTop;
if (yMin > bandTop + bandH - 1) yMin = bandTop + bandH - 1;
LICE_Line(bmp, x, yMin, x, yMax, waveCol, 1.0f, 0, false);
const WaveformColumnSpan s = waveformColumnSpan(
band, compressAmplitudeForDisplay(mm.max), compressAmplitudeForDisplay(mm.min));
LICE_Line(bmp, x, s.bottom, x, s.top, waveCol, 1.0f, 0, false);
if (col > 0) {
const float xPrev = static_cast<float>(x - 1);
const float xF = static_cast<float>(x);
LICE_FLine(bmp, xPrev, static_cast<float>(prevTop), xF,
static_cast<float>(topF), waveCol, 1.0f, 0, true);
LICE_FLine(bmp, xPrev, static_cast<float>(prevBottom), xF,
static_cast<float>(bottomF), waveCol, 1.0f, 0, true);
LICE_FLine(bmp, xPrev, static_cast<float>(prev.topF), xF,
static_cast<float>(s.topF), waveCol, 1.0f, 0, true);
LICE_FLine(bmp, xPrev, static_cast<float>(prev.bottomF), xF,
static_cast<float>(s.bottomF), waveCol, 1.0f, 0, true);
}
prevTop = topF;
prevBottom = bottomF;
prev = s;
}
}
}