Toast Engine
Loading...
Searching...
No Matches
trigger.hpp
Go to the documentation of this file.
1
10
11#pragma once
12
13#include "parse_util.hpp"
14#include "value.hpp"
15
16#include <cmath>
17#include <glm/glm.hpp>
18#include <string_view>
19#include <toml++/toml.hpp>
20
21namespace input {
22
26class TOAST_API ITrigger {
27public:
34 struct Result {
35 bool active = false;
38
39 void emit(ActionEvent event) noexcept { signals.emit(event); }
40 };
41
42 ITrigger() = default;
43 ITrigger(const ITrigger&) = default;
44 auto operator=(const ITrigger&) -> ITrigger& = default;
45 virtual ~ITrigger() = default;
46
53 virtual auto evaluate(const InputSample& sample, const EvalContext& ctx) -> Result = 0;
54
55 [[nodiscard]]
56 virtual auto name() const -> std::string_view = 0;
57
64 static auto fromToml(const toml::table& table, ValueType vt) -> std::shared_ptr<ITrigger>;
65};
66
67namespace detail {
68
69inline auto levelOf(const InputSample& sample) -> float {
70 return sample.is_vector ? glm::length(sample.vector) : std::abs(sample.scalar);
71}
72
73inline auto toNDC(glm::vec2 screen, const EvalContext& ctx) -> glm::vec2 {
74 const glm::vec2 size = glm::max(ctx.viewport_size, glm::vec2 {1.0f});
75 const glm::vec2 local = screen - ctx.viewport_pos;
76 return glm::vec2 {((local.x / size.x) * 2.0f) - 1.0f, 1.0f - ((local.y / size.y) * 2.0f)};
77}
78
79inline auto deltaToNDC(glm::vec2 screen_delta, const EvalContext& ctx) -> glm::vec2 {
80 const glm::vec2 size = glm::max(ctx.viewport_size, glm::vec2 {1.0f});
81 return glm::vec2 {(screen_delta.x / size.x) * 2.0f, -(screen_delta.y / size.y) * 2.0f};
82}
83
84inline auto applyAxisThreshold(float value, glm::vec2 range, bool normalize) -> float {
85 const float mag = std::abs(value);
86 if (mag < range.x) {
87 return 0.0f;
88 }
89 if (!normalize) {
90 return value;
91 }
92 const float span = std::max(range.y - range.x, 0.0001f);
93 const float t = glm::clamp((mag - range.x) / span, 0.0f, 1.0f);
94 return std::copysign(t, value);
95}
96
97}
98
102class StartTrigger : public ITrigger {
103public:
104 StartTrigger(const toml::table& t, ValueType vt)
105 : m_threshold(parse::number(t["threshold"], 0.5f)),
106 m_value(parse::value(t["value"], vt)),
107 m_countdown(parse::number(t["countdown"], 0.0f)) { }
108
109 auto evaluate(const InputSample& sample, const EvalContext& ctx) -> Result override {
110 Result r;
111 const bool on = detail::levelOf(sample) >= m_threshold;
112
113 if (on && !m_was_on) {
114 m_elapsed = 0.0f;
115 if (m_countdown <= 0.0f) {
116 m_started = true;
117 r.active = true;
118 r.value = m_value;
119 r.emit(ActionEvent::start);
120 } else {
121 m_started = false;
122 r.emit(ActionEvent::tries);
123 }
124 } else if (on && !m_started) {
125 m_elapsed += ctx.delta;
126 if (m_elapsed >= m_countdown) {
127 m_started = true;
128 r.active = true;
129 r.value = m_value;
130 r.emit(ActionEvent::start);
131 } else {
133 }
134 } else if (!on && m_was_on) {
135 if (!m_started) {
137 }
138 m_started = false;
139 }
140
141 m_was_on = on;
142 return r;
143 }
144
145 [[nodiscard]]
146 auto name() const -> std::string_view override {
147 return "start";
148 }
149
150private:
151 float m_threshold = 0.5f;
152 Value m_value {true};
153 float m_countdown = 0.0f;
154 bool m_was_on = false;
155 bool m_started = false;
156 float m_elapsed = 0.0f;
157};
158
162class ReleaseTrigger : public ITrigger {
163public:
164 ReleaseTrigger(const toml::table& t, ValueType vt)
165 : m_threshold(parse::number(t["threshold"], 0.5f)),
166 m_value(parse::value(t["value"], vt)) { }
167
168 auto evaluate(const InputSample& sample, const EvalContext& /*ctx*/) -> Result override {
169 Result r;
170 const bool on = detail::levelOf(sample) >= m_threshold;
171 if (!on && m_was_on) {
172 r.active = true;
173 r.value = m_value;
174 r.emit(ActionEvent::release);
175 }
176 m_was_on = on;
177 return r;
178 }
179
180 [[nodiscard]]
181 auto name() const -> std::string_view override {
182 return "release";
183 }
184
185private:
186 float m_threshold = 0.5f;
187 Value m_value {false};
188 bool m_was_on = false;
189};
190
194class HoldTrigger : public ITrigger {
195public:
196 HoldTrigger(const toml::table& t, ValueType vt)
197 : m_threshold_start(parse::number(t["threshold_start"], 0.5f)),
198 m_threshold_release(parse::number(t["threshold_release"], 0.5f)),
199 m_value(parse::value(t["value"], vt)),
200 m_send_start(parse::boolean(t["send_start"], false)),
201 m_send_release(parse::boolean(t["send_release"], false)),
202 m_countdown(parse::number(t["countdown"], 0.0f)) { }
203
204 auto evaluate(const InputSample& sample, const EvalContext& ctx) -> Result override {
205 Result r;
206 const float level = detail::levelOf(sample);
207
208 const bool was_holding = m_holding;
209 if (!m_holding && level >= m_threshold_start) {
210 m_holding = true;
211 m_elapsed = 0.0f;
212 m_started = false;
213 } else if (m_holding && level < m_threshold_release) {
214 m_holding = false;
215 }
216
217 if (m_holding) {
218 if (!m_started) {
219 if (!was_holding && m_countdown <= 0.0f) {
220 m_started = true;
221 } else {
222 m_elapsed += ctx.delta;
223 if (m_elapsed >= m_countdown) {
224 m_started = true;
225 }
226 }
227
228 if (m_started) {
229 if (m_send_start) {
230 r.emit(ActionEvent::start);
231 }
232 } else {
234 }
235 }
236
237 if (m_started) {
238 r.active = true;
239 r.value = m_value;
240 r.emit(ActionEvent::hold);
241 }
242 } else if (was_holding) {
243 if (!m_started) {
245 } else if (m_send_release) {
246 r.emit(ActionEvent::release);
247 }
248 m_started = false;
249 }
250
251 return r;
252 }
253
254 [[nodiscard]]
255 auto name() const -> std::string_view override {
256 return "hold";
257 }
258
259private:
260 float m_threshold_start = 0.5f;
261 float m_threshold_release = 0.5f;
262 Value m_value {true};
263 bool m_send_start = false;
264 bool m_send_release = false;
265 float m_countdown = 0.0f;
266 bool m_holding = false;
267 bool m_started = false;
268 float m_elapsed = 0.0f;
269};
270
274class PulseTrigger : public ITrigger {
275public:
276 PulseTrigger(const toml::table& t, ValueType vt)
277 : m_threshold_start(parse::number(t["threshold_start"], 0.5f)),
278 m_threshold_release(parse::number(t["threshold_release"], 0.5f)),
279 m_value(parse::value(t["value"], vt)),
280 m_rate(std::max(parse::number(t["rate"], 1.0f), 0.0001f)),
281 m_send_start(parse::boolean(t["send_start"], false)),
282 m_send_release(parse::boolean(t["send_release"], false)),
283 m_countdown(parse::number(t["countdown"], 0.0f)) { }
284
285 auto evaluate(const InputSample& sample, const EvalContext& ctx) -> Result override {
286 Result r;
287 const float level = detail::levelOf(sample);
288
289 const bool was_holding = m_holding;
290 if (!m_holding && level >= m_threshold_start) {
291 m_holding = true;
292 m_elapsed = 0.0f;
293 m_pulse_acc = 0.0f;
294 m_started = false;
295 } else if (m_holding && level < m_threshold_release) {
296 m_holding = false;
297 }
298
299 if (m_holding) {
300 if (!m_started) {
301 if (!was_holding && m_countdown <= 0.0f) {
302 m_started = true;
303 } else {
304 m_elapsed += ctx.delta;
305 if (m_elapsed >= m_countdown) {
306 m_started = true;
307 }
308 }
309
310 if (m_started) {
311 m_pulse_acc = 0.0f;
312 r.active = true;
313 r.value = m_value;
314 if (m_send_start) {
315 r.emit(ActionEvent::start);
316 }
317 r.emit(ActionEvent::hold);
318 } else {
320 }
321 } else {
322 m_pulse_acc += ctx.delta;
323 if (m_pulse_acc >= m_rate) {
324 m_pulse_acc -= m_rate;
325 r.active = true;
326 r.value = m_value;
327 r.emit(ActionEvent::hold);
328 }
329 }
330 } else if (was_holding) {
331 if (!m_started) {
333 } else if (m_send_release) {
334 r.emit(ActionEvent::release);
335 }
336 m_started = false;
337 }
338
339 return r;
340 }
341
342 [[nodiscard]]
343 auto name() const -> std::string_view override {
344 return "pulse";
345 }
346
347private:
348 float m_threshold_start = 0.5f;
349 float m_threshold_release = 0.5f;
350 Value m_value {true};
351 float m_rate = 1.0f;
352 bool m_send_start = false;
353 bool m_send_release = false;
354 float m_countdown = 0.0f;
355 bool m_holding = false;
356 bool m_started = false;
357 float m_elapsed = 0.0f;
358 float m_pulse_acc = 0.0f;
359};
360
364class Axis1DTrigger : public ITrigger {
365public:
366 Axis1DTrigger(const toml::table& t, ValueType vt)
367 : m_value_type(vt),
368 m_threshold(parse::vec2(t["threshold"], glm::vec2 {0.2f, 1.0f})),
369 m_normalize(parse::boolean(t["normalize"], false)),
370 m_axis(static_cast<int>(parse::number(t["axis"], 0.0f))),
371 m_send_start(parse::boolean(t["send_start"], false)),
372 m_send_release(parse::boolean(t["send_release"], false)) { }
373
374 auto evaluate(const InputSample& sample, const EvalContext& /*ctx*/) -> Result override {
375 Result r;
376 const float out = detail::applyAxisThreshold(sample.scalar, m_threshold, m_normalize);
377 const bool active = std::abs(sample.scalar) >= m_threshold.x;
378
379 if (active) {
380 if (m_value_type == ValueType::axis_2d) {
381 glm::vec2 v {0.0f};
382 v[m_axis == 1 ? 1 : 0] = out;
383 r.value = Value {v};
384 } else {
385 r.value = Value {out};
386 }
387 r.active = true;
388 if (!m_was_active && m_send_start) {
389 r.emit(ActionEvent::start);
390 }
391 r.emit(ActionEvent::hold);
392 } else if (m_was_active) {
393 if (m_send_release) {
394 r.emit(ActionEvent::release);
395 }
396 // One final hold so listeners see the axis settle at zero
397 r.emit(ActionEvent::hold);
398 }
399
400 m_was_active = active;
401 return r;
402 }
403
404 [[nodiscard]]
405 auto name() const -> std::string_view override {
406 return "axis1d";
407 }
408
409private:
410 ValueType m_value_type = ValueType::axis_1d;
411 glm::vec2 m_threshold {0.2f, 1.0f};
412 bool m_normalize = false;
413 int m_axis = 0;
414 bool m_send_start = false;
415 bool m_send_release = false;
416 bool m_was_active = false;
417};
418
422class Axis2DTrigger : public ITrigger {
423public:
424 Axis2DTrigger(const toml::table& t, ValueType /*vt*/)
425 : m_threshold_x(parse::vec2(t["threshold_x"], glm::vec2 {0.2f, 1.0f})),
426 m_threshold_y(parse::vec2(t["threshold_y"], glm::vec2 {0.2f, 1.0f})),
427 m_normalize(parse::boolean(t["normalize"], false)),
428 m_send_start(parse::boolean(t["send_start"], false)),
429 m_send_release(parse::boolean(t["send_release"], false)) { }
430
431 auto evaluate(const InputSample& sample, const EvalContext& /*ctx*/) -> Result override {
432 Result r;
433 const glm::vec2 out {
434 detail::applyAxisThreshold(sample.vector.x, m_threshold_x, m_normalize),
435 detail::applyAxisThreshold(sample.vector.y, m_threshold_y, m_normalize),
436 };
437 const bool active = out.x != 0.0f || out.y != 0.0f;
438
439 if (active) {
440 r.active = true;
441 r.value = Value {out};
442 if (!m_was_active && m_send_start) {
443 r.emit(ActionEvent::start);
444 }
445 r.emit(ActionEvent::hold);
446 } else if (m_was_active) {
447 if (m_send_release) {
448 r.emit(ActionEvent::release);
449 }
450 // One final hold so listeners see the axis settle at zero
451 r.emit(ActionEvent::hold);
452 }
453
454 m_was_active = active;
455 return r;
456 }
457
458 [[nodiscard]]
459 auto name() const -> std::string_view override {
460 return "axis2d";
461 }
462
463private:
464 glm::vec2 m_threshold_x {0.2f, 1.0f};
465 glm::vec2 m_threshold_y {0.2f, 1.0f};
466 bool m_normalize = false;
467 bool m_send_start = false;
468 bool m_send_release = false;
469 bool m_was_active = false;
470};
471
475class CursorTrigger : public ITrigger {
476 // TODO: Camera and world space fall back to NDC until renderer camera access is wired
477
478public:
479 CursorTrigger(const toml::table& t, ValueType /*vt*/)
480 : m_space(static_cast<int>(parse::number(t["space"], 1.0f))),
481 m_delta(parse::boolean(t["delta"], false)) { }
482
483 auto evaluate(const InputSample& sample, const EvalContext& ctx) -> Result override {
484 Result r;
485 const glm::vec2 screen = sample.vector;
486
487 glm::vec2 measured = screen;
488 if (m_delta) {
489 measured = m_has_last ? (screen - m_last) : glm::vec2 {0.0f};
490 m_last = screen;
491 m_has_last = true;
492 }
493
494 glm::vec2 out;
495 if (m_space == 0) {
496 out = measured; // screen space: raw pixels
497 } else {
498 // TODO: implement camera (2) and world (3) space projection once camera access is wired
499 out = m_delta ? detail::deltaToNDC(measured, ctx) : detail::toNDC(measured, ctx);
500 }
501
502 // Only report changes; a delta run ends with a single (0, 0) report
503 if (!m_has_reported || out != m_reported) {
504 m_reported = out;
505 m_has_reported = true;
506 r.active = true;
507 r.value = Value {out};
508 r.emit(ActionEvent::hold);
509 }
510 return r;
511 }
512
513 [[nodiscard]]
514 auto name() const -> std::string_view override {
515 return "cursor";
516 }
517
518private:
519 int m_space = 1;
520 bool m_delta = false;
521 glm::vec2 m_last {0.0f};
522 bool m_has_last = false;
523 glm::vec2 m_reported {0.0f};
524 bool m_has_reported = false;
525};
526
527}
auto evaluate(const InputSample &sample, const EvalContext &) -> Result override
Evaluates the trigger against the current input sample.
Definition trigger.hpp:374
auto evaluate(const InputSample &sample, const EvalContext &) -> Result override
Evaluates the trigger against the current input sample.
Definition trigger.hpp:431
auto evaluate(const InputSample &sample, const EvalContext &ctx) -> Result override
Evaluates the trigger against the current input sample.
Definition trigger.hpp:483
auto evaluate(const InputSample &sample, const EvalContext &ctx) -> Result override
Evaluates the trigger against the current input sample.
Definition trigger.hpp:204
Polymorphic base for all triggers.
Definition trigger.hpp:26
static auto fromToml(const toml::table &table, ValueType vt) -> std::shared_ptr< ITrigger >
Builds a trigger from a TOML table keyed by its "name" field.
Definition trigger.cpp:7
virtual auto evaluate(const InputSample &sample, const EvalContext &ctx) -> Result=0
Evaluates the trigger against the current input sample.
auto evaluate(const InputSample &sample, const EvalContext &ctx) -> Result override
Evaluates the trigger against the current input sample.
Definition trigger.hpp:285
auto evaluate(const InputSample &sample, const EvalContext &) -> Result override
Evaluates the trigger against the current input sample.
Definition trigger.hpp:168
auto evaluate(const InputSample &sample, const EvalContext &ctx) -> Result override
Evaluates the trigger against the current input sample.
Definition trigger.hpp:109
Small helpers shared by the TOML parsing paths of triggers, modifiers and binds.
Per-frame context passed down to triggers and modifiers during evaluation.
Definition value.hpp:140
glm::vec2 viewport_size
size of the active viewport in pixels, for cursor NDC conversion
Definition value.hpp:143
glm::vec2 viewport_pos
top-left of the active viewport in pixels
Definition value.hpp:144
Outcome of evaluating a trigger for one frame.
Definition trigger.hpp:34
Value value
value contributed while active
Definition trigger.hpp:36
bool active
whether the trigger contributes its value this frame
Definition trigger.hpp:35
SignalList signals
discrete events fired this frame
Definition trigger.hpp:37
One frame of raw input for a single keycode.
Definition value.hpp:130
Fixed-capacity buffer of discrete events fired in a single frame.
Definition value.hpp:90
A tagged value that may represent a bool, a float or a glm::vec2.
Definition value.hpp:31
Value type shared by triggers, modifiers and actionsg.
ValueType
Value domain of an action or an input sample.
Definition value.hpp:22
@ axis_2d
vector
Definition value.hpp:25
ActionEvent
Discrete event an action can emit, carried alongside the action in an InputEvent.
Definition value.hpp:76
@ release
the input went from on to off
Definition value.hpp:79
@ tries
the input was pressed but a countdown must elapse before it starts
Definition value.hpp:80
@ cancelled
the input was released before the countdown finished
Definition value.hpp:82
@ countdown
equivalent of hold while the start countdown is running
Definition value.hpp:81
@ hold
the input is held; sent continuously or on a pulse
Definition value.hpp:78
@ start
the input went from off to on
Definition value.hpp:77