Skip to content
Permalink
Browse files

DolphinQt/ControllerEmu: Replace Input Radius/Shape settings with an …

…input calibration "wizard".
  • Loading branch information...
jordan-woyak committed Feb 5, 2019
1 parent 46918f4 commit 0064f70c8a17cd40c15486ac385508ce0ee20cf1
@@ -5,6 +5,7 @@
#pragma once

#include <array>
#include <cmath>

// Tiny matrix/vector library.
// Used for things like Free-Look in the gfx backend.
@@ -39,6 +40,71 @@ inline Vec3 operator+(Vec3 lhs, const Vec3& rhs)
return lhs += rhs;
}

template <typename T>
union TVec2
{
TVec2() = default;
TVec2(T _x, T _y) : data{_x, _y} {}

T Cross(const TVec2& rhs) const { return (x * rhs.y) - (y * rhs.x); }
T Dot(const TVec2& rhs) const { return (x * rhs.x) + (y * rhs.y); }
T LengthSquared() const { return Dot(*this); }
T Length() const { return std::sqrt(LengthSquared()); }
TVec2 Normalized() const { return *this / Length(); }

TVec2& operator+=(const TVec2& rhs)
{
x += rhs.x;
y += rhs.y;
return *this;
}

TVec2& operator-=(const TVec2& rhs)
{
x -= rhs.x;
y -= rhs.y;
return *this;
}

TVec2& operator*=(T scalar)
{
x *= scalar;
y *= scalar;
return *this;
}

TVec2 operator-() const { return {-x, -y}; }

std::array<T, 2> data = {};

struct
{
T x;
T y;
};
};

template <typename T>
TVec2<T> operator+(TVec2<T> lhs, const TVec2<T>& rhs)
{
return lhs += rhs;
}

template <typename T>
TVec2<T> operator-(TVec2<T> lhs, const TVec2<T>& rhs)
{
return lhs -= rhs;
}

template <typename T>
TVec2<T> operator*(TVec2<T> lhs, T scalar)
{
return lhs *= scalar;
}

using Vec2 = TVec2<float>;
using DVec2 = TVec2<double>;

class Matrix33
{
public:
@@ -16,6 +16,8 @@
#include "InputCommon/ControllerEmu/ControlGroup/ControlGroup.h"
#include "InputCommon/ControllerEmu/ControlGroup/MixedTriggers.h"
#include "InputCommon/ControllerEmu/Setting/BooleanSetting.h"
#include "InputCommon/ControllerEmu/StickGate.h"

#include "InputCommon/GCPadStatus.h"

static const u16 button_bitmasks[] = {
@@ -248,6 +250,10 @@ void GCPad::LoadDefaults(const ControllerInterface& ciface)
m_main_stick->SetControlExpression(4, "Shift_L"); // Modifier
#endif

// Because our defaults use keyboard input, set calibration shapes to squares.
m_c_stick->SetCalibrationFromGate(ControllerEmu::SquareStickGate(1.0));
m_main_stick->SetCalibrationFromGate(ControllerEmu::SquareStickGate(1.0));

// Triggers
m_triggers->SetControlExpression(0, "Q"); // L
m_triggers->SetControlExpression(1, "W"); // R
@@ -203,6 +203,9 @@ void Nunchuk::LoadDefaults(const ControllerInterface& ciface)
m_stick->SetControlExpression(2, "A"); // left
m_stick->SetControlExpression(3, "D"); // right

// Because our defaults use keyboard input, set calibration shape to a square.
m_stick->SetCalibrationFromGate(ControllerEmu::SquareStickGate(1.0));

// Buttons
#ifdef _WIN32
m_buttons->SetControlExpression(0, "LCONTROL"); // C
@@ -6,7 +6,11 @@

#include <array>
#include <cmath>
#include <numeric>

#include <QAction>
#include <QDateTime>
#include <QMessageBox>
#include <QPainter>
#include <QTimer>

@@ -48,9 +52,9 @@ MappingIndicator::MappingIndicator(ControllerEmu::ControlGroup* group) : m_group
{
setMinimumHeight(128);

m_timer = new QTimer(this);
connect(m_timer, &QTimer::timeout, this, [this] { repaint(); });
m_timer->start(1000 / 30);
const auto timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, [this] { repaint(); });
timer->start(1000 / 30);
}

namespace
@@ -75,6 +79,49 @@ QPolygonF GetPolygonFromRadiusGetter(F&& radius_getter, double scale)

return shape;
}

// Used to check if the user seems to have attempted proper calibration.
bool IsCalibrationDataSensible(const ControllerEmu::ReshapableInput::CalibrationData& data)
{
// Test that the average input radius is not below a threshold.
// This will make sure the user has actually moved their stick from neutral.

// Even the GC controller's small range would pass this test.
constexpr double REASONABLE_AVERAGE_RADIUS = 0.6;

const double sum = std::accumulate(data.begin(), data.end(), 0.0);
const double mean = sum / data.size();

if (mean < REASONABLE_AVERAGE_RADIUS)
{
return false;
}

// Test that the standard deviation is below a threshold.
// This will make sure the user has not just filled in one side of their input.

// Approx. deviation of a square input gate, anything much more than that would be unusual.
constexpr double REASONABLE_DEVIATION = 0.14;

// Population standard deviation.
const double square_sum = std::inner_product(data.begin(), data.end(), data.begin(), 0.0);
const double standard_deviation = std::sqrt(square_sum / data.size() - mean * mean);

return standard_deviation < REASONABLE_DEVIATION;
}

// Used to test for a miscalibrated stick so the user can be informed.
bool IsPointOutsideCalibration(Common::DVec2 point, ControllerEmu::ReshapableInput& input)
{
const double current_radius = point.Length();
const double input_radius =
input.GetInputRadiusAtAngle(std::atan2(point.y, point.x) + MathUtil::TAU);

constexpr double ALLOWED_ERROR = 1.3;

return current_radius > input_radius * ALLOWED_ERROR;
}

} // namespace

void MappingIndicator::DrawCursor(ControllerEmu::Cursor& cursor)
@@ -89,6 +136,8 @@ void MappingIndicator::DrawCursor(ControllerEmu::Cursor& cursor)
const auto adj_coord = cursor.GetState(true);
Settings::Instance().SetControllerStateNeeded(false);

UpdateCalibrationWidget({raw_coord.x, raw_coord.y});

// Bounding box size:
const double scale = height() / 2.5;

@@ -107,6 +156,12 @@ void MappingIndicator::DrawCursor(ControllerEmu::Cursor& cursor)
p.setRenderHint(QPainter::Antialiasing, true);
p.setRenderHint(QPainter::SmoothPixmapTransform, true);

if (IsCalibrating())
{
DrawCalibration(p, {raw_coord.x, raw_coord.y});
return;
}

// Deadzone for Z (forward/backward):
const double deadzone = cursor.numeric_settings[cursor.SETTING_DEADZONE]->GetValue();
if (deadzone > 0.0)
@@ -198,6 +253,8 @@ void MappingIndicator::DrawReshapableInput(ControllerEmu::ReshapableInput& stick
const auto adj_coord = stick.GetReshapableState(true);
Settings::Instance().SetControllerStateNeeded(false);

UpdateCalibrationWidget(raw_coord);

// Bounding box size:
const double scale = height() / 2.5;

@@ -216,6 +273,12 @@ void MappingIndicator::DrawReshapableInput(ControllerEmu::ReshapableInput& stick
p.setRenderHint(QPainter::Antialiasing, true);
p.setRenderHint(QPainter::SmoothPixmapTransform, true);

if (IsCalibrating())
{
DrawCalibration(p, raw_coord);
return;
}

// Input gate. (i.e. the octagon shape)
p.setPen(gate_pen_color);
p.setBrush(gate_brush_color);
@@ -363,3 +426,149 @@ void MappingIndicator::paintEvent(QPaintEvent*)
break;
}
}

void MappingIndicator::DrawCalibration(QPainter& p, Common::DVec2 point)
{
// TODO: Ugly magic number used in a few places in this file.
const double scale = height() / 2.5;

// Input shape.
p.setPen(INPUT_SHAPE_PEN);
p.setBrush(Qt::NoBrush);
p.drawPolygon(GetPolygonFromRadiusGetter(
[this](double angle) { return m_calibration_widget->GetCalibrationRadiusAtAngle(angle); },
scale));

// Stick position.
p.setPen(Qt::NoPen);
p.setBrush(ADJ_INPUT_COLOR);
p.drawEllipse(QPointF{point.x, point.y} * scale, INPUT_DOT_RADIUS, INPUT_DOT_RADIUS);
}

void MappingIndicator::UpdateCalibrationWidget(Common::DVec2 point)
{
if (m_calibration_widget)
m_calibration_widget->Update(point);
}

bool MappingIndicator::IsCalibrating() const
{
return m_calibration_widget && m_calibration_widget->IsCalibrating();
}

void MappingIndicator::SetCalibrationWidget(CalibrationWidget* widget)
{
m_calibration_widget = widget;
}

CalibrationWidget::CalibrationWidget(ControllerEmu::ReshapableInput& input,
MappingIndicator& indicator)
: m_input(input), m_indicator(indicator), m_completion_action{}
{
m_indicator.SetCalibrationWidget(this);

// Make it more apparent that this is a menu with more options.
setPopupMode(ToolButtonPopupMode::MenuButtonPopup);

SetupActions();

setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed);

m_informative_timer = new QTimer(this);
connect(m_informative_timer, &QTimer::timeout, this, [this] {
// If the user has started moving we'll assume they know what they are doing.
if (*std::max_element(m_calibration_data.begin(), m_calibration_data.end()) > 0.5)
return;

QMessageBox msg(QMessageBox::Information, tr("Calibration"),
tr("For best results please slowly move your input to all possible regions."),
QMessageBox::Ok, this);
msg.setWindowModality(Qt::WindowModal);
msg.exec();
});
m_informative_timer->setSingleShot(true);
}

void CalibrationWidget::SetupActions()
{
const auto calibrate_action = new QAction(tr("Calibrate"), this);
const auto reset_action = new QAction(tr("Reset"), this);

connect(calibrate_action, &QAction::triggered, [this]() { StartCalibration(); });
connect(reset_action, &QAction::triggered, [this]() { m_input.SetCalibrationToDefault(); });

for (auto* action : actions())
removeAction(action);

addAction(calibrate_action);
addAction(reset_action);
setDefaultAction(calibrate_action);

m_completion_action = new QAction(tr("Finish Calibration"), this);
connect(m_completion_action, &QAction::triggered, [this]() {
m_input.SetCalibrationData(std::move(m_calibration_data));
m_informative_timer->stop();
SetupActions();
});
}

void CalibrationWidget::StartCalibration()
{
m_calibration_data.assign(m_input.CALIBRATION_SAMPLE_COUNT, 0.0);

// Cancel calibration.
const auto cancel_action = new QAction(tr("Cancel Calibration"), this);
connect(cancel_action, &QAction::triggered, [this]() {
m_calibration_data.clear();
m_informative_timer->stop();
SetupActions();
});

for (auto* action : actions())
removeAction(action);

addAction(cancel_action);
addAction(m_completion_action);
setDefaultAction(cancel_action);

// If the user doesn't seem to know what they are doing after a bit inform them.
m_informative_timer->start(2000);
}

void CalibrationWidget::Update(Common::DVec2 point)
{
QFont f = parentWidget()->font();
QPalette p = parentWidget()->palette();

if (IsCalibrating())
{
m_input.UpdateCalibrationData(m_calibration_data, point);

if (IsCalibrationDataSensible(m_calibration_data))
{
setDefaultAction(m_completion_action);
}
}
else if (IsPointOutsideCalibration(point, m_input))
{
// Flashing bold and red on miscalibration.
if (QDateTime::currentDateTime().toMSecsSinceEpoch() % 500 < 350)
{
f.setBold(true);
p.setColor(QPalette::ButtonText, Qt::red);
}
}

setFont(f);
setPalette(p);
}

bool CalibrationWidget::IsCalibrating() const
{
return !m_calibration_data.empty();
}

double CalibrationWidget::GetCalibrationRadiusAtAngle(double angle) const
{
return m_input.GetCalibrationDataRadiusAtAngle(m_calibration_data, angle);
}

0 comments on commit 0064f70

Please sign in to comment.
You can’t perform that action at this time.