1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
|
// Copyright 2018 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "InputCommon/ControllerEmu/StickGate.h"
#include <algorithm>
#include <cmath>
#include <fmt/format.h>
#include "Common/Common.h"
#include "Common/MathUtil.h"
#include "Common/Matrix.h"
#include "Common/StringUtil.h"
#include "InputCommon/ControllerEmu/Control/Control.h"
#include "InputCommon/ControllerEmu/Setting/NumericSetting.h"
namespace
{
constexpr auto CALIBRATION_CONFIG_NAME = "Calibration";
constexpr auto CALIBRATION_DEFAULT_VALUE = 1.0;
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 segment defined by two points.
std::optional<double> GetRayLineIntersection(Common::DVec2 ray, Common::DVec2 point1,
Common::DVec2 point2)
{
const auto diff = point2 - point1;
const auto dot = diff.Dot({-ray.y, ray.x});
if (std::abs(dot) < 0.00001)
{
// Both points are on top of eachother.
return std::nullopt;
}
const auto segment_position = point1.Dot({ray.y, -ray.x}) / dot;
if (segment_position < -0.00001 || segment_position > 1.00001)
{
// Ray does not pass through segment.
return std::nullopt;
}
return diff.Cross(-point1) / dot;
}
double GetNearestNotch(double angle, double virtual_notch_angle)
{
constexpr auto sides = 8;
constexpr auto rounding = MathUtil::TAU / sides;
const auto closest_notch = std::round(angle / rounding) * rounding;
const auto angle_diff =
std::fmod(angle - closest_notch + MathUtil::PI, MathUtil::TAU) - MathUtil::PI;
return std::abs(angle_diff) < virtual_notch_angle / 2 ? closest_notch : angle;
}
Common::DVec2 GetPointFromAngleAndLength(double angle, double length)
{
return Common::DVec2{std::cos(angle), std::sin(angle)} * length;
}
} // namespace
namespace ControllerEmu
{
constexpr int ReshapableInput::CALIBRATION_SAMPLE_COUNT;
std::optional<u32> StickGate::GetIdealCalibrationSampleCount() const
{
return std::nullopt;
}
OctagonStickGate::OctagonStickGate(ControlState radius) : m_radius(radius)
{
}
ControlState OctagonStickGate::GetRadiusAtAngle(double angle) const
{
constexpr int sides = 8;
constexpr double sum_int_angles = (sides - 2) * MathUtil::PI;
constexpr double half_int_angle = sum_int_angles / sides / 2;
angle = std::fmod(angle, MathUtil::TAU / sides);
// Solve ASA triangle using The Law of Sines:
return m_radius / std::sin(MathUtil::PI - angle - half_int_angle) * std::sin(half_int_angle);
}
std::optional<u32> OctagonStickGate::GetIdealCalibrationSampleCount() const
{
return 8;
}
RoundStickGate::RoundStickGate(ControlState radius) : m_radius(radius)
{
}
ControlState RoundStickGate::GetRadiusAtAngle(double) const
{
return m_radius;
}
std::optional<u32> RoundStickGate::GetIdealCalibrationSampleCount() const
{
// The "radius" is the same at every angle so a single sample is enough.
return 1;
}
SquareStickGate::SquareStickGate(ControlState half_width) : m_half_width(half_width)
{
}
ControlState SquareStickGate::GetRadiusAtAngle(double angle) const
{
constexpr double section_angle = MathUtil::TAU / 4;
return m_half_width /
std::cos(std::fmod(angle + section_angle / 2, section_angle) - section_angle / 2);
}
std::optional<u32> SquareStickGate::GetIdealCalibrationSampleCount() const
{
// Because angle:0 points to the right we must use 8 samples for our square.
return 8;
}
ReshapableInput::ReshapableInput(std::string name_, std::string ui_name_, GroupType type_)
: ControlGroup(std::move(name_), std::move(ui_name_), type_)
{
// 50 is not always enough but users can set it to more with an expression
AddDeadzoneSetting(&m_deadzone_setting, 50);
}
ControlState ReshapableInput::GetDeadzoneRadiusAtAngle(double angle) const
{
// FYI: deadzone is scaled by input radius which allows the shape to match.
return GetInputRadiusAtAngle(angle) * GetDeadzonePercentage();
}
ControlState ReshapableInput::GetInputRadiusAtAngle(double angle) const
{
// Handle the "default" state.
if (m_calibration.empty())
{
return GetDefaultInputRadiusAtAngle(angle);
}
return GetCalibrationDataRadiusAtAngle(m_calibration, angle);
}
ControlState ReshapableInput::GetDeadzonePercentage() const
{
return m_deadzone_setting.GetValue() / 100;
}
ControlState ReshapableInput::GetCalibrationDataRadiusAtAngle(const CalibrationData& data,
double angle)
{
const auto sample_pos = angle / MathUtil::TAU * data.size();
// Interpolate the radius between 2 calibration samples.
const u32 sample1_index = u32(sample_pos) % data.size();
const u32 sample2_index = (sample1_index + 1) % data.size();
const double sample1_angle = sample1_index * MathUtil::TAU / data.size();
const double sample2_angle = sample2_index * MathUtil::TAU / data.size();
const auto intersection =
GetRayLineIntersection(GetPointFromAngleAndLength(angle, 1.0),
GetPointFromAngleAndLength(sample1_angle, data[sample1_index]),
GetPointFromAngleAndLength(sample2_angle, data[sample2_index]));
// Intersection has no value when points are on top of eachother.
return intersection.value_or(data[sample1_index]);
}
ControlState ReshapableInput::GetDefaultInputRadiusAtAngle(double angle) const
{
// This will normally be the same as the gate radius.
// Unless a sub-class is doing weird things with the gate radius (e.g. Tilt)
return GetGateRadiusAtAngle(angle);
}
void ReshapableInput::SetCalibrationToDefault()
{
m_calibration.clear();
}
void ReshapableInput::SetCalibrationFromGate(const StickGate& gate)
{
m_calibration.resize(gate.GetIdealCalibrationSampleCount().value_or(CALIBRATION_SAMPLE_COUNT));
u32 i = 0;
for (auto& val : m_calibration)
val = gate.GetRadiusAtAngle(MathUtil::TAU * i++ / m_calibration.size());
}
void ReshapableInput::UpdateCalibrationData(CalibrationData& data, Common::DVec2 point1,
Common::DVec2 point2)
{
for (u32 i = 0; i != data.size(); ++i)
{
const auto angle = i * MathUtil::TAU / data.size();
const auto intersection =
GetRayLineIntersection(GetPointFromAngleAndLength(angle, 1.0), point1, point2);
data[i] = std::max(data[i], intersection.value_or(data[i]));
}
}
const ReshapableInput::CalibrationData& ReshapableInput::GetCalibrationData() const
{
return m_calibration;
}
void ReshapableInput::SetCalibrationData(CalibrationData 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(Common::IniFile::Section* section,
const std::string& default_device, const std::string& base_name)
{
ControlGroup::LoadConfig(section, default_device, base_name);
const std::string group(base_name + name + '/');
// Special handling for "Modifier" button "Range" settings which default to 50% instead of 100%.
if (const auto* modifier_input = GetModifierInput())
{
section->Get(group + modifier_input->name + "/Range", &modifier_input->control_ref->range,
50.0);
modifier_input->control_ref->range /= 100;
}
std::string load_str;
section->Get(group + CALIBRATION_CONFIG_NAME, &load_str, "");
const auto load_data = SplitString(load_str, ' ');
m_calibration.assign(load_data.size(), CALIBRATION_DEFAULT_VALUE);
auto it = load_data.begin();
for (auto& sample : m_calibration)
{
if (TryParse(*(it++), &sample))
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(Common::IniFile::Section* section,
const std::string& default_device, const std::string& base_name)
{
ControlGroup::SaveConfig(section, default_device, base_name);
const std::string group(base_name + name + '/');
// Special handling for "Modifier" button "Range" settings which default to 50% instead of 100%.
if (const auto* modifier_input = GetModifierInput())
{
section->Set(group + modifier_input->name + "/Range", modifier_input->control_ref->range * 100,
50.0);
}
std::vector<std::string> save_data(m_calibration.size());
std::transform(
m_calibration.begin(), m_calibration.end(), save_data.begin(),
[](ControlState val) { return fmt::format("{:.2f}", val * CALIBRATION_CONFIG_SCALE); });
section->Set(group + CALIBRATION_CONFIG_NAME, JoinStrings(save_data, " "), "");
// Save center value.
static constexpr char center_format[] = "{:.2f} {:.2f}";
const auto center_data = fmt::format(center_format, m_center.x * CENTER_CONFIG_SCALE,
m_center.y * CENTER_CONFIG_SCALE);
section->Set(group + CENTER_CONFIG_NAME, center_data, fmt::format(center_format, 0.0, 0.0));
}
ReshapableInput::ReshapeData ReshapableInput::Reshape(ControlState x, ControlState y,
ControlState modifier,
ControlState clamp) const
{
x -= m_center.x;
y -= m_center.y;
// We run this even if both x and y will be zero.
// In that case, std::atan2(0, 0) returns a valid non-NaN value, but the exact value
// (which depends on the signs of x and y) does not matter here as dist is zero
// TODO: make the AtAngle functions work with negative angles:
ControlState angle = std::atan2(y, x) + MathUtil::TAU;
const ControlState input_max_dist = GetInputRadiusAtAngle(angle);
ControlState gate_max_dist = GetGateRadiusAtAngle(angle);
// If input radius (from calibration) is zero apply no scaling to prevent division by zero.
const ControlState max_dist = input_max_dist ? input_max_dist : gate_max_dist;
ControlState dist = Common::DVec2{x, y}.Length() / max_dist;
const double virtual_notch_size = GetVirtualNotchSize();
if (virtual_notch_size > 0.0 && dist >= MINIMUM_NOTCH_DISTANCE)
{
angle = GetNearestNotch(angle, virtual_notch_size);
gate_max_dist = GetGateRadiusAtAngle(angle);
}
// If the modifier is pressed, scale the distance by the modifier's value.
// This is affected by the modifier's "range" setting which defaults to 50%.
if (modifier)
{
dist *= modifier;
}
// Apply deadzone as a percentage of the user-defined calibration shape/size:
dist = ApplyDeadzone(dist, GetDeadzonePercentage());
// Scale to the gate shape/radius:
dist *= gate_max_dist;
return {std::clamp(std::cos(angle) * dist, -clamp, clamp),
std::clamp(std::sin(angle) * dist, -clamp, clamp)};
}
Control* ReshapableInput::GetModifierInput() const
{
return nullptr;
}
} // namespace ControllerEmu
|