ControllerEmu: Add support for setting the center of a ReshapableInput

This is useful in far out-of-calibration controllers, such as the
Switch Pro controller. This also adds support for configuring the center
in the Mapping widget.
This commit is contained in:
Artemis Tosini
2019-04-12 20:26:34 +00:00
parent d60b0c6b37
commit 49e46c8aff
4 changed files with 112 additions and 19 deletions

View File

@ -29,6 +29,7 @@
namespace namespace
{ {
const QColor CENTER_COLOR = Qt::blue;
const QColor C_STICK_GATE_COLOR = Qt::yellow; const QColor C_STICK_GATE_COLOR = Qt::yellow;
const QColor CURSOR_TV_COLOR = 0xaed6f1; const QColor CURSOR_TV_COLOR = 0xaed6f1;
const QColor TILT_GATE_COLOR = 0xa2d9ce; const QColor TILT_GATE_COLOR = 0xa2d9ce;
@ -115,7 +116,8 @@ namespace
{ {
// Constructs a polygon by querying a radius at varying angles: // Constructs a polygon by querying a radius at varying angles:
template <typename F> template <typename F>
QPolygonF GetPolygonFromRadiusGetter(F&& radius_getter, double scale) QPolygonF GetPolygonFromRadiusGetter(F&& radius_getter, double scale,
Common::DVec2 center = {0.0, 0.0})
{ {
// A multiple of 8 (octagon) and enough points to be visibly pleasing: // A multiple of 8 (octagon) and enough points to be visibly pleasing:
constexpr int shape_point_count = 32; constexpr int shape_point_count = 32;
@ -127,7 +129,8 @@ QPolygonF GetPolygonFromRadiusGetter(F&& radius_getter, double scale)
const double angle = MathUtil::TAU * p / shape.size(); const double angle = MathUtil::TAU * p / shape.size();
const double radius = radius_getter(angle) * scale; const double radius = radius_getter(angle) * scale;
point = {std::cos(angle) * radius, std::sin(angle) * radius}; point = {std::cos(angle) * radius + center.x * scale,
std::sin(angle) * radius + center.y * scale};
++p; ++p;
} }
@ -167,9 +170,10 @@ bool IsCalibrationDataSensible(const ControllerEmu::ReshapableInput::Calibration
// Used to test for a miscalibrated stick so the user can be informed. // Used to test for a miscalibrated stick so the user can be informed.
bool IsPointOutsideCalibration(Common::DVec2 point, ControllerEmu::ReshapableInput& input) bool IsPointOutsideCalibration(Common::DVec2 point, ControllerEmu::ReshapableInput& input)
{ {
const double current_radius = point.Length(); const auto center = input.GetCenter();
const double input_radius = const double current_radius = (point - center).Length();
input.GetInputRadiusAtAngle(std::atan2(point.y, point.x) + MathUtil::TAU); const double input_radius = input.GetInputRadiusAtAngle(
std::atan2(point.y - center.y, point.x - center.x) + MathUtil::TAU);
constexpr double ALLOWED_ERROR = 1.3; constexpr double ALLOWED_ERROR = 1.3;
@ -180,6 +184,8 @@ bool IsPointOutsideCalibration(Common::DVec2 point, ControllerEmu::ReshapableInp
void MappingIndicator::DrawCursor(ControllerEmu::Cursor& cursor) void MappingIndicator::DrawCursor(ControllerEmu::Cursor& cursor)
{ {
const auto center = cursor.GetCenter();
const QColor tv_brush_color = CURSOR_TV_COLOR; const QColor tv_brush_color = CURSOR_TV_COLOR;
const QColor tv_pen_color = tv_brush_color.darker(125); const QColor tv_pen_color = tv_brush_color.darker(125);
@ -247,13 +253,21 @@ void MappingIndicator::DrawCursor(ControllerEmu::Cursor& cursor)
p.setPen(GetDeadZonePen()); p.setPen(GetDeadZonePen());
p.setBrush(GetDeadZoneBrush()); p.setBrush(GetDeadZoneBrush());
p.drawPolygon(GetPolygonFromRadiusGetter( p.drawPolygon(GetPolygonFromRadiusGetter(
[&cursor](double ang) { return cursor.GetDeadzoneRadiusAtAngle(ang); }, scale)); [&cursor](double ang) { return cursor.GetDeadzoneRadiusAtAngle(ang); }, scale, center));
// Input shape. // Input shape.
p.setPen(GetInputShapePen()); p.setPen(GetInputShapePen());
p.setBrush(Qt::NoBrush); p.setBrush(Qt::NoBrush);
p.drawPolygon(GetPolygonFromRadiusGetter( p.drawPolygon(GetPolygonFromRadiusGetter(
[&cursor](double ang) { return cursor.GetInputRadiusAtAngle(ang); }, scale)); [&cursor](double ang) { return cursor.GetInputRadiusAtAngle(ang); }, scale, center));
// Center.
if (center.x || center.y)
{
p.setPen(Qt::NoPen);
p.setBrush(CENTER_COLOR);
p.drawEllipse(QPointF{center.x, center.y} * scale, INPUT_DOT_RADIUS, INPUT_DOT_RADIUS);
}
// Raw stick position. // Raw stick position.
p.setPen(Qt::NoPen); p.setPen(Qt::NoPen);
@ -276,6 +290,8 @@ void MappingIndicator::DrawReshapableInput(ControllerEmu::ReshapableInput& stick
const bool is_c_stick = m_group->name == "C-Stick"; const bool is_c_stick = m_group->name == "C-Stick";
const bool is_tilt = m_group->name == "Tilt"; const bool is_tilt = m_group->name == "Tilt";
const auto center = stick.GetCenter();
QColor gate_brush_color = GetGateColor(); QColor gate_brush_color = GetGateColor();
if (is_c_stick) if (is_c_stick)
@ -321,7 +337,7 @@ void MappingIndicator::DrawReshapableInput(ControllerEmu::ReshapableInput& stick
if (IsCalibrating()) if (IsCalibrating())
{ {
DrawCalibration(p, raw_coord); DrawCalibration(p, raw_coord, center);
return; return;
} }
@ -335,13 +351,21 @@ void MappingIndicator::DrawReshapableInput(ControllerEmu::ReshapableInput& stick
p.setPen(GetDeadZonePen()); p.setPen(GetDeadZonePen());
p.setBrush(GetDeadZoneBrush()); p.setBrush(GetDeadZoneBrush());
p.drawPolygon(GetPolygonFromRadiusGetter( p.drawPolygon(GetPolygonFromRadiusGetter(
[&stick](double ang) { return stick.GetDeadzoneRadiusAtAngle(ang); }, scale)); [&stick](double ang) { return stick.GetDeadzoneRadiusAtAngle(ang); }, scale, center));
// Input shape. // Input shape.
p.setPen(GetInputShapePen()); p.setPen(GetInputShapePen());
p.setBrush(Qt::NoBrush); p.setBrush(Qt::NoBrush);
p.drawPolygon(GetPolygonFromRadiusGetter( p.drawPolygon(GetPolygonFromRadiusGetter(
[&stick](double ang) { return stick.GetInputRadiusAtAngle(ang); }, scale)); [&stick](double ang) { return stick.GetInputRadiusAtAngle(ang); }, scale, center));
// Center.
if (center.x || center.y)
{
p.setPen(Qt::NoPen);
p.setBrush(CENTER_COLOR);
p.drawEllipse(QPointF{center.x, center.y} * scale, INPUT_DOT_RADIUS, INPUT_DOT_RADIUS);
}
// Raw stick position. // Raw stick position.
p.setPen(Qt::NoPen); p.setPen(Qt::NoPen);
@ -454,6 +478,8 @@ void MappingIndicator::DrawMixedTriggers()
void MappingIndicator::DrawForce(ControllerEmu::Force& force) void MappingIndicator::DrawForce(ControllerEmu::Force& force)
{ {
const auto center = force.GetCenter();
const QColor gate_brush_color = SWING_GATE_COLOR; const QColor gate_brush_color = SWING_GATE_COLOR;
const QColor gate_pen_color = gate_brush_color.darker(125); const QColor gate_pen_color = gate_brush_color.darker(125);
@ -528,13 +554,21 @@ void MappingIndicator::DrawForce(ControllerEmu::Force& force)
p.setPen(GetDeadZoneColor()); p.setPen(GetDeadZoneColor());
p.setBrush(GetDeadZoneBrush()); p.setBrush(GetDeadZoneBrush());
p.drawPolygon(GetPolygonFromRadiusGetter( p.drawPolygon(GetPolygonFromRadiusGetter(
[&force](double ang) { return force.GetDeadzoneRadiusAtAngle(ang); }, scale)); [&force](double ang) { return force.GetDeadzoneRadiusAtAngle(ang); }, scale, center));
// Input shape. // Input shape.
p.setPen(GetInputShapePen()); p.setPen(GetInputShapePen());
p.setBrush(Qt::NoBrush); p.setBrush(Qt::NoBrush);
p.drawPolygon(GetPolygonFromRadiusGetter( p.drawPolygon(GetPolygonFromRadiusGetter(
[&force](double ang) { return force.GetInputRadiusAtAngle(ang); }, scale)); [&force](double ang) { return force.GetInputRadiusAtAngle(ang); }, scale, center));
// Center.
if (center.x || center.y)
{
p.setPen(Qt::NoPen);
p.setBrush(CENTER_COLOR);
p.drawEllipse(QPointF{center.x, center.y} * scale, INPUT_DOT_RADIUS, INPUT_DOT_RADIUS);
}
// Raw stick position. // Raw stick position.
p.setPen(Qt::NoPen); p.setPen(Qt::NoPen);
@ -665,8 +699,7 @@ void ShakeMappingIndicator::DrawShake()
p.drawPolyline(polyline); p.drawPolyline(polyline);
} }
} }
void MappingIndicator::DrawCalibration(QPainter& p, Common::DVec2 point, Common::DVec2 center)
void MappingIndicator::DrawCalibration(QPainter& p, Common::DVec2 point)
{ {
// Bounding box size: // Bounding box size:
const double scale = GetScale(); const double scale = GetScale();
@ -676,7 +709,7 @@ void MappingIndicator::DrawCalibration(QPainter& p, Common::DVec2 point)
p.setBrush(Qt::NoBrush); p.setBrush(Qt::NoBrush);
p.drawPolygon(GetPolygonFromRadiusGetter( p.drawPolygon(GetPolygonFromRadiusGetter(
[this](double angle) { return m_calibration_widget->GetCalibrationRadiusAtAngle(angle); }, [this](double angle) { return m_calibration_widget->GetCalibrationRadiusAtAngle(angle); },
scale)); scale, center));
// Stick position. // Stick position.
p.setPen(Qt::NoPen); p.setPen(Qt::NoPen);
@ -729,15 +762,24 @@ CalibrationWidget::CalibrationWidget(ControllerEmu::ReshapableInput& input,
void CalibrationWidget::SetupActions() void CalibrationWidget::SetupActions()
{ {
const auto calibrate_action = new QAction(tr("Calibrate"), this); const auto calibrate_action = new QAction(tr("Calibrate"), this);
const auto center_action = new QAction(tr("Center and Calibrate"), this);
const auto reset_action = new QAction(tr("Reset"), this); const auto reset_action = new QAction(tr("Reset"), this);
connect(calibrate_action, &QAction::triggered, [this]() { StartCalibration(); }); connect(calibrate_action, &QAction::triggered, [this]() { StartCalibration(); });
connect(reset_action, &QAction::triggered, [this]() { m_input.SetCalibrationToDefault(); }); connect(center_action, &QAction::triggered, [this]() {
StartCalibration();
m_is_centering = true;
});
connect(reset_action, &QAction::triggered, [this]() {
m_input.SetCalibrationToDefault();
m_input.SetCenter({0, 0});
});
for (auto* action : actions()) for (auto* action : actions())
removeAction(action); removeAction(action);
addAction(calibrate_action); addAction(calibrate_action);
addAction(center_action);
addAction(reset_action); addAction(reset_action);
setDefaultAction(calibrate_action); setDefaultAction(calibrate_action);
@ -751,11 +793,15 @@ void CalibrationWidget::SetupActions()
void CalibrationWidget::StartCalibration() void CalibrationWidget::StartCalibration()
{ {
// Set the old center so we can revert
m_old_center = m_input.GetCenter();
m_calibration_data.assign(m_input.CALIBRATION_SAMPLE_COUNT, 0.0); m_calibration_data.assign(m_input.CALIBRATION_SAMPLE_COUNT, 0.0);
// Cancel calibration. // Cancel calibration.
const auto cancel_action = new QAction(tr("Cancel Calibration"), this); const auto cancel_action = new QAction(tr("Cancel Calibration"), this);
connect(cancel_action, &QAction::triggered, [this]() { connect(cancel_action, &QAction::triggered, [this]() {
m_input.SetCenter(m_old_center);
m_calibration_data.clear(); m_calibration_data.clear();
m_informative_timer->stop(); m_informative_timer->stop();
SetupActions(); SetupActions();
@ -777,9 +823,14 @@ void CalibrationWidget::Update(Common::DVec2 point)
QFont f = parentWidget()->font(); QFont f = parentWidget()->font();
QPalette p = parentWidget()->palette(); QPalette p = parentWidget()->palette();
if (IsCalibrating()) if (m_is_centering)
{ {
m_input.UpdateCalibrationData(m_calibration_data, point); m_input.SetCenter(point);
m_is_centering = false;
}
else if (IsCalibrating())
{
m_input.UpdateCalibrationData(m_calibration_data, point - m_input.GetCenter());
if (IsCalibrationDataSensible(m_calibration_data)) if (IsCalibrationDataSensible(m_calibration_data))
{ {

View File

@ -55,7 +55,7 @@ private:
void DrawReshapableInput(ControllerEmu::ReshapableInput& stick); void DrawReshapableInput(ControllerEmu::ReshapableInput& stick);
void DrawMixedTriggers(); void DrawMixedTriggers();
void DrawForce(ControllerEmu::Force&); void DrawForce(ControllerEmu::Force&);
void DrawCalibration(QPainter& p, Common::DVec2 point); void DrawCalibration(QPainter& p, Common::DVec2 point, Common::DVec2 center = {0, 0});
void paintEvent(QPaintEvent*) override; void paintEvent(QPaintEvent*) override;
@ -101,4 +101,7 @@ private:
QAction* m_completion_action; QAction* m_completion_action;
ControllerEmu::ReshapableInput::CalibrationData m_calibration_data; ControllerEmu::ReshapableInput::CalibrationData m_calibration_data;
QTimer* m_informative_timer; QTimer* m_informative_timer;
bool m_is_centering = false;
ControllerEmu::ReshapableInput::ReshapeData m_old_center;
}; };

View File

@ -20,6 +20,9 @@ constexpr auto CALIBRATION_CONFIG_NAME = "Calibration";
constexpr auto CALIBRATION_DEFAULT_VALUE = 1.0; constexpr auto CALIBRATION_DEFAULT_VALUE = 1.0;
constexpr auto CALIBRATION_CONFIG_SCALE = 100; constexpr auto CALIBRATION_CONFIG_SCALE = 100;
constexpr auto CENTER_CONFIG_NAME = "Center";
constexpr auto CENTER_CONFIG_SCALE = 100;
// Calculate distance to intersection of a ray with a line defined by two points. // Calculate distance to intersection of a ray with a line defined by two points.
double GetRayLineIntersection(Common::DVec2 ray, Common::DVec2 point1, Common::DVec2 point2) double GetRayLineIntersection(Common::DVec2 ray, Common::DVec2 point1, Common::DVec2 point2)
{ {
@ -214,6 +217,16 @@ void ReshapableInput::SetCalibrationData(CalibrationData data)
m_calibration = std::move(data); m_calibration = std::move(data);
} }
const ReshapableInput::ReshapeData& ReshapableInput::GetCenter() const
{
return m_center;
}
void ReshapableInput::SetCenter(ReshapableInput::ReshapeData center)
{
m_center = center;
}
void ReshapableInput::LoadConfig(IniFile::Section* section, const std::string& default_device, void ReshapableInput::LoadConfig(IniFile::Section* section, const std::string& default_device,
const std::string& base_name) const std::string& base_name)
{ {
@ -232,6 +245,20 @@ void ReshapableInput::LoadConfig(IniFile::Section* section, const std::string& d
if (TryParse(*(it++), &sample)) if (TryParse(*(it++), &sample))
sample /= CALIBRATION_CONFIG_SCALE; sample /= CALIBRATION_CONFIG_SCALE;
} }
section->Get(group + CENTER_CONFIG_NAME, &load_str, "");
const auto center_data = SplitString(load_str, ' ');
m_center = Common::DVec2();
if (center_data.size() == 2)
{
if (TryParse(center_data[0], &m_center.x))
m_center.x /= CENTER_CONFIG_SCALE;
if (TryParse(center_data[1], &m_center.y))
m_center.y /= CENTER_CONFIG_SCALE;
}
} }
void ReshapableInput::SaveConfig(IniFile::Section* section, const std::string& default_device, void ReshapableInput::SaveConfig(IniFile::Section* section, const std::string& default_device,
@ -245,11 +272,19 @@ void ReshapableInput::SaveConfig(IniFile::Section* section, const std::string& d
m_calibration.begin(), m_calibration.end(), save_data.begin(), m_calibration.begin(), m_calibration.end(), save_data.begin(),
[](ControlState val) { return StringFromFormat("%.2f", val * CALIBRATION_CONFIG_SCALE); }); [](ControlState val) { return StringFromFormat("%.2f", val * CALIBRATION_CONFIG_SCALE); });
section->Set(group + CALIBRATION_CONFIG_NAME, JoinStrings(save_data, " "), ""); section->Set(group + CALIBRATION_CONFIG_NAME, JoinStrings(save_data, " "), "");
const auto center_data = StringFromFormat("%.2f %.2f", m_center.x * CENTER_CONFIG_SCALE,
m_center.y * CENTER_CONFIG_SCALE);
section->Set(group + CENTER_CONFIG_NAME, center_data, "");
} }
ReshapableInput::ReshapeData ReshapableInput::Reshape(ControlState x, ControlState y, ReshapableInput::ReshapeData ReshapableInput::Reshape(ControlState x, ControlState y,
ControlState modifier) ControlState modifier)
{ {
x -= m_center.x;
y -= m_center.y;
// TODO: make the AtAngle functions work with negative angles: // TODO: make the AtAngle functions work with negative angles:
const ControlState angle = std::atan2(y, x) + MathUtil::TAU; const ControlState angle = std::atan2(y, x) + MathUtil::TAU;

View File

@ -97,6 +97,9 @@ public:
const CalibrationData& GetCalibrationData() const; const CalibrationData& GetCalibrationData() const;
void SetCalibrationData(CalibrationData data); void SetCalibrationData(CalibrationData data);
const ReshapeData& GetCenter() const;
void SetCenter(ReshapeData center);
protected: protected:
ReshapeData Reshape(ControlState x, ControlState y, ControlState modifier = 0.0); ReshapeData Reshape(ControlState x, ControlState y, ControlState modifier = 0.0);
@ -106,6 +109,7 @@ private:
CalibrationData m_calibration; CalibrationData m_calibration;
SettingValue<double> m_deadzone_setting; SettingValue<double> m_deadzone_setting;
ReshapeData m_center;
}; };
} // namespace ControllerEmu } // namespace ControllerEmu