Browse Source

Implement scaled linear and rotary knob modes.

tags/v2.0.0
Andrew Belt 4 years ago
parent
commit
b93e889c75
10 changed files with 182 additions and 69 deletions
  1. +8
    -2
      include/app/Knob.hpp
  2. +4
    -7
      include/app/SliderKnob.hpp
  3. +0
    -3
      include/app/SvgKnob.hpp
  4. +5
    -5
      include/settings.hpp
  5. +119
    -32
      src/app/Knob.cpp
  6. +12
    -11
      src/app/MenuBar.cpp
  7. +23
    -0
      src/app/SliderKnob.cpp
  8. +6
    -5
      src/app/SvgKnob.cpp
  9. +3
    -2
      src/event.cpp
  10. +2
    -2
      src/settings.cpp

+ 8
- 2
include/app/Knob.hpp View File

@@ -13,14 +13,19 @@ struct Knob : ParamWidget {
struct Internal;
Internal* internal;

/** Multiplier for mouse movement to adjust knob value */
float speed = 1.0;
/** Drag horizontally instead of vertically. */
bool horizontal = false;
/** Enables per-sample value smoothing while dragging. */
bool smooth = true;
/** DEPRECATED. Use `ParamQuantity::snapEnabled`. */
bool snap = false;
/** Multiplier for mouse movement to adjust knob value */
float speed = 1.f;
/** Force dragging to linear, e.g. for sliders. */
bool forceLinear = false;
/** Angles in radians. */
float minAngle = -M_PI;
float maxAngle = M_PI;

Knob();
~Knob();
@@ -30,6 +35,7 @@ struct Knob : ParamWidget {
void onDragStart(const event::DragStart& e) override;
void onDragEnd(const event::DragEnd& e) override;
void onDragMove(const event::DragMove& e) override;
void onDragLeave(const event::DragLeave& e) override;
};




+ 4
- 7
include/app/SliderKnob.hpp View File

@@ -8,13 +8,10 @@ namespace app {


struct SliderKnob : Knob {
// Bypass Knob's circular hitbox detection
void onHover(const event::Hover& e) override {
ParamWidget::onHover(e);
}
void onButton(const event::Button& e) override {
ParamWidget::onButton(e);
}
SliderKnob();

void onHover(const event::Hover& e) override;
void onButton(const event::Button& e) override;
};




+ 0
- 3
include/app/SvgKnob.hpp View File

@@ -17,9 +17,6 @@ struct SvgKnob : Knob {
CircularShadow* shadow;
widget::TransformWidget* tw;
widget::SvgWidget* sw;
/** Angles in radians */
float minAngle = 0.f;
float maxAngle = M_PI;

SvgKnob();
void setSvg(std::shared_ptr<Svg> svg);


+ 5
- 5
include/settings.hpp View File

@@ -28,12 +28,12 @@ extern bool invertZoom;
extern float cableOpacity;
extern float cableTension;
enum KnobMode {
KNOB_MODE_LINEAR_LOCKED = 0,
KNOB_MODE_LINEAR,
KNOB_MODE_LINEAR_UNLOCKED,
KNOB_MODE_LINEAR_SPEED,
KNOB_MODE_LINEAR_SPEED_UNLOCKED,
KNOB_MODE_CIRCULAR_ABSOLUTE,
KNOB_MODE_CIRCULAR_RELATIVE,
KNOB_MODE_SCALED_LINEAR_LOCKED = 100,
KNOB_MODE_SCALED_LINEAR,
KNOB_MODE_ROTARY_ABSOLUTE = 200,
KNOB_MODE_ROTARY_RELATIVE,
};
extern KnobMode knobMode;
extern float sampleRate;


+ 119
- 32
src/app/Knob.cpp View File

@@ -3,20 +3,26 @@
#include <app/Scene.hpp>
#include <random.hpp>
#include <history.hpp>
#include <settings.hpp>


namespace rack {
namespace app {


static const float KNOB_SENSITIVITY = 0.0015f;


struct Knob::Internal {
/** Value of the knob before dragging. */
float oldValue = 0.f;
/** Fractional value between the param's value and the dragged knob position. */
/** Fractional value between the param's value and the dragged knob position.
Using a "snapValue" variable and rounding is insufficient because the mouse needs to reach 1.0, not 0.5 to obtain the first increment.
*/
float snapDelta = 0.f;

/** Speed multiplier in speed knob mode */
float linearScale = 1.f;
/** The mouse has once escaped from the knob while dragging. */
bool rotaryDragEnabled = false;
float dragAngle = NAN;
};


@@ -38,6 +44,7 @@ void Knob::initParamQuantity() {
}

void Knob::onHover(const event::Hover& e) {
// Only call super if mouse position is in the circle
math::Vec c = box.size.div(2);
float dist = e.pos.minus(c).norm();
if (dist <= c.x) {
@@ -63,14 +70,27 @@ void Knob::onDragStart(const event::DragStart& e) {
internal->snapDelta = 0.f;
}

APP->window->cursorLock();
settings::KnobMode km = settings::knobMode;
if (km == settings::KNOB_MODE_LINEAR_LOCKED || km == settings::KNOB_MODE_SCALED_LINEAR_LOCKED) {
APP->window->cursorLock();
}
// Only changed for KNOB_MODE_LINEAR_*.
internal->linearScale = 1.f;
// Only used for KNOB_MODE_ROTARY_*.
internal->rotaryDragEnabled = false;
internal->dragAngle = NAN;

ParamWidget::onDragStart(e);
}

void Knob::onDragEnd(const event::DragEnd& e) {
if (e.button != GLFW_MOUSE_BUTTON_LEFT)
return;

APP->window->cursorUnlock();
settings::KnobMode km = settings::knobMode;
if (km == settings::KNOB_MODE_LINEAR_LOCKED || km == settings::KNOB_MODE_SCALED_LINEAR_LOCKED) {
APP->window->cursorUnlock();
}

engine::ParamQuantity* pq = getParamQuantity();
if (pq) {
@@ -86,56 +106,123 @@ void Knob::onDragEnd(const event::DragEnd& e) {
APP->history->push(h);
}
}

ParamWidget::onDragEnd(e);
}

void Knob::onDragMove(const event::DragMove& e) {
if (e.button != GLFW_MOUSE_BUTTON_LEFT)
return;

settings::KnobMode km = settings::knobMode;
bool linearMode = (km < settings::KNOB_MODE_ROTARY_ABSOLUTE) || forceLinear;

engine::ParamQuantity* pq = getParamQuantity();
if (pq) {
float range;
if (pq->isBounded()) {
range = pq->getRange();
}
else {
// Continuous encoders scale as if their limits are +/-1
range = 2.f;
}
float delta = (horizontal ? e.mouseDelta.x : -e.mouseDelta.y);
delta *= KNOB_SENSITIVITY;
delta *= speed;
delta *= range;
float value = smooth ? pq->getSmoothValue() : pq->getValue();

// Drag slower if Mod is held
// Scale by mod keys
float modScale = 1.f;
// Drag slower if Ctrl is held
int mods = APP->window->getMods();
if ((mods & RACK_MOD_MASK) == RACK_MOD_CTRL) {
delta /= 16.f;
modScale /= 16.f;
}
// Drag even slower if Mod-Shift is held
// Drag even slower if Ctrl-Shift is held
if ((mods & RACK_MOD_MASK) == (RACK_MOD_CTRL | GLFW_MOD_SHIFT)) {
delta /= 256.f;
modScale /= 256.f;
}

if (pq->snapEnabled) {
// Replace delta with an accumulated delta since the last integer knob.
internal->snapDelta += delta;
delta = std::trunc(internal->snapDelta);
internal->snapDelta -= delta;
// Ratio between parameter value scale / (angle range / 2*pi)
float rangeRatio;
if (pq->isBounded()) {
rangeRatio = pq->getRange();
rangeRatio /= (maxAngle - minAngle) / float(2 * M_PI);
}
else {
rangeRatio = 1.f;
}

// Set value
if (smooth) {
pq->setSmoothValue(pq->getSmoothValue() + delta);
if (linearMode) {
float delta = (horizontal ? e.mouseDelta.x : -e.mouseDelta.y);
// TODO Put this in settings
const float linearSensitivity = 1 / 1000.f;
delta *= linearSensitivity;
delta *= modScale;
delta *= rangeRatio;

// Scale delta if in scaled linear knob mode
if (km == settings::KNOB_MODE_SCALED_LINEAR_LOCKED || km == settings::KNOB_MODE_SCALED_LINEAR) {
float deltaY = (horizontal ? -e.mouseDelta.y : -e.mouseDelta.x);
const float pixelTau = 200.f;
internal->linearScale *= std::pow(2.f, deltaY / pixelTau);
delta *= internal->linearScale;
}

// Handle value snapping
if (pq->snapEnabled) {
// Replace delta with an accumulated delta since the last integer knob.
internal->snapDelta += delta;
delta = std::trunc(internal->snapDelta);
internal->snapDelta -= delta;
}

value += delta;
}
else {
pq->setValue(pq->getValue() + delta);
else if (internal->rotaryDragEnabled) {
math::Vec origin = getAbsoluteOffset(box.size.div(2));
math::Vec deltaPos = APP->scene->mousePos.minus(origin);
float angle = deltaPos.arg() + float(M_PI) / 2;

bool absoluteRotaryMode = (km == settings::KNOB_MODE_ROTARY_ABSOLUTE) && pq->isBounded();
if (absoluteRotaryMode) {
// Find angle closest to midpoint of angle range, mod 2*pi
float midAngle = (minAngle + maxAngle) / 2;
angle = math::eucMod(angle - midAngle + float(M_PI), float(2 * M_PI)) + midAngle - float(M_PI);
value = math::rescale(angle, minAngle, maxAngle, pq->getMinValue(), pq->getMaxValue());
}
else {
if (!std::isfinite(internal->dragAngle)) {
// Set the starting angle
internal->dragAngle = angle;
}

// Find angle closest to last angle, mod 2*pi
float deltaAngle = math::eucMod(angle - internal->dragAngle + float(M_PI), float(2 * M_PI)) - float(M_PI);
internal->dragAngle = angle;
float delta = deltaAngle / float(2 * M_PI) * rangeRatio;
delta *= modScale;

// Handle value snapping
if (pq->snapEnabled) {
// Replace delta with an accumulated delta since the last integer knob.
internal->snapDelta += delta;
delta = std::trunc(internal->snapDelta);
internal->snapDelta -= delta;
}

value += delta;
}
}

// Set value
if (smooth)
pq->setSmoothValue(value);
else
pq->setValue(value);
}

ParamWidget::onDragMove(e);
}

void Knob::onDragLeave(const event::DragLeave& e) {
if (e.origin == this) {
internal->rotaryDragEnabled = true;
}

ParamWidget::onDragLeave(e);
}


} // namespace app
} // namespace rack

+ 12
- 11
src/app/MenuBar.cpp View File

@@ -19,6 +19,7 @@
#include <updater.hpp>
#include <osdialog.h>
#include <thread>
#include <utility>


namespace rack {
@@ -327,19 +328,19 @@ struct KnobModeItem : ui::MenuItem {
ui::Menu* createChildMenu() override {
ui::Menu* menu = new ui::Menu;

static const std::string knobModeNames[] = {
"Linear (locked cursor)",
"Linear",
"Adjustable linear (locked cursor)",
"Adjustable linear",
"Absolute rotary",
"Relative rotary",
static const std::vector<std::pair<settings::KnobMode, std::string>> knobModes = {
{settings::KNOB_MODE_LINEAR_LOCKED, "Linear (locked cursor)"},
{settings::KNOB_MODE_LINEAR, "Linear"},
{settings::KNOB_MODE_SCALED_LINEAR_LOCKED, "Scaled linear (locked cursor)"},
{settings::KNOB_MODE_SCALED_LINEAR, "Scaled linear"},
{settings::KNOB_MODE_ROTARY_ABSOLUTE, "Absolute rotary"},
{settings::KNOB_MODE_ROTARY_RELATIVE, "Relative rotary"},
};
for (int i = 0; i < (int) LENGTHOF(knobModeNames); i++) {
for (const auto& pair : knobModes) {
KnobModeValueItem* item = new KnobModeValueItem;
item->knobMode = (settings::KnobMode) i;
item->text = knobModeNames[i];
item->rightText = CHECKMARK(settings::knobMode == i);
item->knobMode = pair.first;
item->text = pair.second;
item->rightText = CHECKMARK(settings::knobMode == pair.first);
menu->addChild(item);
}
return menu;


+ 23
- 0
src/app/SliderKnob.cpp View File

@@ -0,0 +1,23 @@
#include <app/SliderKnob.hpp>


namespace rack {
namespace app {


SliderKnob::SliderKnob() {
forceLinear = true;
}

void SliderKnob::onHover(const event::Hover& e) {
// Bypass Knob's circular hitbox detection
ParamWidget::onHover(e);
}

void SliderKnob::onButton(const event::Button& e) {
ParamWidget::onButton(e);
}


} // namespace app
} // namespace rack

+ 6
- 5
src/app/SvgKnob.cpp View File

@@ -38,14 +38,15 @@ void SvgKnob::onChange(const event::Change& e) {
float value = pq->getSmoothValue();
float angle;
if (!pq->isBounded()) {
// Center unbounded knobs
angle = math::rescale(value, -1.f, 1.f, minAngle, maxAngle);
// Number of rotations equals value for unbounded range
angle = value * (2 * M_PI);
}
else if (pq->getMinValue() == pq->getMaxValue()) {
// Center 0 range
angle = math::rescale(0.f, -1.f, 1.f, minAngle, maxAngle);
else if (pq->getRange() == 0.f) {
// Center angle for zero range
angle = (minAngle + maxAngle) / 2.f;
}
else {
// Proportional angle for finite range
angle = math::rescale(value, pq->getMinValue(), pq->getMaxValue(), minAngle, maxAngle);
}
angle = std::fmod(angle, 2 * M_PI);


+ 3
- 2
src/event.cpp View File

@@ -233,8 +233,9 @@ bool State::handleHover(math::Vec pos, math::Vec mouseDelta) {

bool State::handleLeave() {
heldKeys.clear();
setDragHovered(NULL);
setHovered(NULL);
// When leaving the window, don't un-hover widgets because the mouse might be dragging.
// setDragHovered(NULL);
// setHovered(NULL);
return true;
}



+ 2
- 2
src/settings.cpp View File

@@ -22,7 +22,7 @@ float zoom = 0.0;
bool invertZoom = false;
float cableOpacity = 0.5;
float cableTension = 0.5;
KnobMode knobMode = KNOB_MODE_LINEAR;
KnobMode knobMode = KNOB_MODE_LINEAR_LOCKED;
float sampleRate = 44100.0;
int threadCount = 1;
bool paramTooltip = false;
@@ -145,7 +145,7 @@ void fromJson(json_t* rootJ) {
json_t* allowCursorLockJ = json_object_get(rootJ, "allowCursorLock");
if (allowCursorLockJ) {
if (json_is_false(allowCursorLockJ))
knobMode = KNOB_MODE_LINEAR_UNLOCKED;
knobMode = KNOB_MODE_LINEAR;
}

json_t* knobModeJ = json_object_get(rootJ, "knobMode");


Loading…
Cancel
Save