From 000ebd009f0e884947f3a9f0eceee700e2c36f99 Mon Sep 17 00:00:00 2001 From: Ink Open Source Date: Wed, 31 Jul 2024 10:32:40 -0700 Subject: [PATCH] Add LoopContractionMitigation to the StrokeModeler. This is behind a param flag that is default disabled at least until we have values we are confident in. This also involves changing the way that the Query() point is projected onto the segment and thus is behind a default-off param flag. PiperOrigin-RevId: 658058054 --- ink_stroke_modeler/BUILD.bazel | 32 +- ink_stroke_modeler/internal/BUILD.bazel | 37 +- .../loop_contraction_mitigation_modeler.cc | 71 + .../loop_contraction_mitigation_modeler.h | 50 + ...oop_contraction_mitigation_modeler_test.cc | 134 + .../internal/stylus_state_modeler.cc | 132 +- .../internal/stylus_state_modeler.h | 42 +- .../internal/stylus_state_modeler_test.cc | 911 ++++-- ..._state_modeler_with_new_projection_test.cc | 750 +++++ ink_stroke_modeler/params.cc | 57 +- ink_stroke_modeler/params.h | 55 +- ink_stroke_modeler/params_test.cc | 59 +- ink_stroke_modeler/stroke_modeler.cc | 103 +- ink_stroke_modeler/stroke_modeler.h | 2 + ink_stroke_modeler/stroke_modeler_test.cc | 99 +- ...stroke_modeler_with_new_projection_test.cc | 2591 +++++++++++++++++ ink_stroke_modeler/type_matchers.cc | 47 + ink_stroke_modeler/type_matchers.h | 15 + ink_stroke_modeler/types.h | 13 + 19 files changed, 4867 insertions(+), 333 deletions(-) create mode 100644 ink_stroke_modeler/internal/loop_contraction_mitigation_modeler.cc create mode 100644 ink_stroke_modeler/internal/loop_contraction_mitigation_modeler.h create mode 100644 ink_stroke_modeler/internal/loop_contraction_mitigation_modeler_test.cc create mode 100644 ink_stroke_modeler/internal/stylus_state_modeler_with_new_projection_test.cc create mode 100644 ink_stroke_modeler/stroke_modeler_with_new_projection_test.cc create mode 100644 ink_stroke_modeler/type_matchers.cc create mode 100644 ink_stroke_modeler/type_matchers.h diff --git a/ink_stroke_modeler/BUILD.bazel b/ink_stroke_modeler/BUILD.bazel index 87af565..9fe77c6 100644 --- a/ink_stroke_modeler/BUILD.bazel +++ b/ink_stroke_modeler/BUILD.bazel @@ -58,8 +58,10 @@ cc_library( ":params", ":types", "//ink_stroke_modeler/internal:internal_types", + "//ink_stroke_modeler/internal:loop_contraction_mitigation_modeler", "//ink_stroke_modeler/internal:position_modeler", "//ink_stroke_modeler/internal:stylus_state_modeler", + "//ink_stroke_modeler/internal:utils", "//ink_stroke_modeler/internal:wobble_smoother", "//ink_stroke_modeler/internal/prediction:input_predictor", "//ink_stroke_modeler/internal/prediction:kalman_predictor", @@ -87,10 +89,22 @@ cc_test( deps = [ ":params", ":stroke_modeler", + ":type_matchers", + ":types", + "@com_google_absl//absl/status", + "@com_google_googletest//:gtest_main", + ], +) + +cc_test( + name = "stroke_modeler_with_new_projection_test", + srcs = ["stroke_modeler_with_new_projection_test.cc"], + deps = [ + ":params", + ":stroke_modeler", + ":type_matchers", ":types", - "//ink_stroke_modeler/internal:type_matchers", "@com_google_absl//absl/status", - "@com_google_absl//absl/strings", "@com_google_googletest//:gtest_main", ], ) @@ -109,6 +123,20 @@ cc_library( ], ) +cc_library( + name = "type_matchers", + testonly = 1, + srcs = ["type_matchers.cc"], + hdrs = ["type_matchers.h"], + deps = [ + ":types", + "//:gtest_for_library_testonly", + "//ink_stroke_modeler/internal:type_matchers", + "@com_google_absl//absl/strings", + "@com_google_googletest//:gtest_for_library", + ], +) + cc_test( name = "types_test", srcs = ["types_test.cc"], diff --git a/ink_stroke_modeler/internal/BUILD.bazel b/ink_stroke_modeler/internal/BUILD.bazel index a68cc7e..5f99fc4 100644 --- a/ink_stroke_modeler/internal/BUILD.bazel +++ b/ink_stroke_modeler/internal/BUILD.bazel @@ -65,9 +65,23 @@ cc_test( deps = [ ":internal_types", ":stylus_state_modeler", - ":type_matchers", "//ink_stroke_modeler:numbers", "//ink_stroke_modeler:params", + "//ink_stroke_modeler:type_matchers", + "//ink_stroke_modeler:types", + "@com_google_googletest//:gtest_main", + ], +) + +cc_test( + name = "stylus_state_modeler_with_new_projection_test", + srcs = ["stylus_state_modeler_with_new_projection_test.cc"], + deps = [ + ":stylus_state_modeler", + "//ink_stroke_modeler:numbers", + "//ink_stroke_modeler:params", + "//ink_stroke_modeler:type_matchers", + "//ink_stroke_modeler:types", "@com_google_googletest//:gtest_main", ], ) @@ -170,3 +184,24 @@ cc_test( "@com_google_googletest//:gtest_main", ], ) + +cc_library( + name = "loop_contraction_mitigation_modeler", + srcs = ["loop_contraction_mitigation_modeler.cc"], + hdrs = ["loop_contraction_mitigation_modeler.h"], + deps = [ + ":utils", + "//ink_stroke_modeler:params", + "//ink_stroke_modeler:types", + ], +) + +cc_test( + name = "loop_contraction_mitigation_modeler_test", + srcs = ["loop_contraction_mitigation_modeler_test.cc"], + deps = [ + ":loop_contraction_mitigation_modeler", + "//ink_stroke_modeler:params", + "@com_google_googletest//:gtest_main", + ], +) diff --git a/ink_stroke_modeler/internal/loop_contraction_mitigation_modeler.cc b/ink_stroke_modeler/internal/loop_contraction_mitigation_modeler.cc new file mode 100644 index 0000000..a3395c7 --- /dev/null +++ b/ink_stroke_modeler/internal/loop_contraction_mitigation_modeler.cc @@ -0,0 +1,71 @@ +#include "ink_stroke_modeler/internal/loop_contraction_mitigation_modeler.h" + +#include + +#include "ink_stroke_modeler/internal/utils.h" +#include "ink_stroke_modeler/params.h" +#include "ink_stroke_modeler/types.h" + +namespace ink { +namespace stroke_model { + +namespace { + +float InverseLerp(float a, float b, float value) { + // If the interval between `a` and `b` is 0, there is no way to get to `t` + // because in the other direction the value of `t` won't impact the result. + if (b - a == 0.f) { + return 0.f; + } + return (value - a) / (b - a); +} + +} // namespace + +void LoopContractionMitigationModeler::Reset( + const PositionModelerParams::LoopContractionMitigationParameters& params) { + speeds_.clear(); + + save_active_ = false; + + params_ = params; +} + +float LoopContractionMitigationModeler::GetInterpolationValue() { + if (speeds_.empty() || !params_.is_enabled) return 1; + + float sum = 0; + for (const auto& speed : speeds_) { + sum += speed; + } + float average_speed = sum / speeds_.size(); + + float source_ratio = std::max( + 0.f, + std::min(1.f, InverseLerp(params_.speed_lower_bound, + params_.speed_upper_bound, average_speed))); + return Interp(params_.interpolation_strength_at_speed_lower_bound, + params_.interpolation_strength_at_speed_upper_bound, + source_ratio); +} + +float LoopContractionMitigationModeler::Update(Vec2 velocity) { + // The moving average acts as a low-pass signal filter, removing + // high-frequency fluctuations in the velocity. + speeds_.push_back(velocity.Magnitude()); + if (speeds_.size() > params_.n_speed_samples) speeds_.pop_front(); + + return GetInterpolationValue(); +} + +void LoopContractionMitigationModeler::Save() { + saved_speeds_ = speeds_; + save_active_ = true; +} + +void LoopContractionMitigationModeler::Restore() { + if (save_active_) speeds_ = saved_speeds_; +} + +} // namespace stroke_model +} // namespace ink diff --git a/ink_stroke_modeler/internal/loop_contraction_mitigation_modeler.h b/ink_stroke_modeler/internal/loop_contraction_mitigation_modeler.h new file mode 100644 index 0000000..858e4fc --- /dev/null +++ b/ink_stroke_modeler/internal/loop_contraction_mitigation_modeler.h @@ -0,0 +1,50 @@ +#ifndef INK_STROKE_MODELER_INTERNAL_LOOP_CONTRACTION_MITIGATION_MODELER_H_ +#define INK_STROKE_MODELER_INTERNAL_LOOP_CONTRACTION_MITIGATION_MODELER_H_ + +#include + +#include "ink_stroke_modeler/params.h" +#include "ink_stroke_modeler/types.h" + +namespace ink { +namespace stroke_model { + +class LoopContractionMitigationModeler { + public: + void Reset( + const PositionModelerParams::LoopContractionMitigationParameters ¶ms); + + // Updates the model with the position and time from the raw inputs, and + // returns the interpolation value to be used when applying the loop + // contraction mitigation + float Update(Vec2 velocity); + + // Returns the interpolation value based on the current set of available + // velocities and the LoopContractionMitigationParameters. + float GetInterpolationValue(); + + // Saves the current state of the modeler. See comment on + // StrokeModeler::Save() for more details. + void Save(); + + // Restores the saved state of the modeler. See comment on + // StrokeModeler::Restore() for more details. + void Restore(); + + private: + std::deque speeds_; + + // Use a deque + bool instead of optional for performance. A + // std::deque, which has a non-trivial destructor that would deallocate its + // capacity. This setup avoids extra calls to the destructor that would be + // triggered by each call to std::optional::reset(). + std::deque saved_speeds_; + bool save_active_ = false; + + PositionModelerParams::LoopContractionMitigationParameters params_; +}; + +} // namespace stroke_model +} // namespace ink + +#endif // INK_STROKE_MODELER_INTERNAL_LOOP_CONTRACTION_MITIGATION_MODELER_H_ diff --git a/ink_stroke_modeler/internal/loop_contraction_mitigation_modeler_test.cc b/ink_stroke_modeler/internal/loop_contraction_mitigation_modeler_test.cc new file mode 100644 index 0000000..b11cd5e --- /dev/null +++ b/ink_stroke_modeler/internal/loop_contraction_mitigation_modeler_test.cc @@ -0,0 +1,134 @@ +#include "ink_stroke_modeler/internal/loop_contraction_mitigation_modeler.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "ink_stroke_modeler/params.h" + +namespace ink { +namespace stroke_model { +namespace { + +using ::testing::FloatNear; +using LoopContractionMitigationParameters = + PositionModelerParams::LoopContractionMitigationParameters; + +const LoopContractionMitigationParameters kDefaultParams{ + .is_enabled = true, + .speed_lower_bound = 0, + .speed_upper_bound = 100, + .interpolation_strength_at_speed_lower_bound = 1, + .interpolation_strength_at_speed_upper_bound = 0, + .n_speed_samples = 5}; + +TEST(LoopContractionMitigationModelerTest, + GetInterpolationValueOnEmptyModeler) { + LoopContractionMitigationModeler modeler; + modeler.Reset(kDefaultParams); + + EXPECT_THAT(modeler.GetInterpolationValue(), FloatNear(1, 0.01)); +} + +TEST(LoopContractionMitigationModelerTest, UpdateWithOneSample) { + LoopContractionMitigationModeler modeler; + modeler.Reset(kDefaultParams); + + EXPECT_THAT(modeler.Update({3, 4}), FloatNear(0.95, 0.01)); + EXPECT_THAT(modeler.GetInterpolationValue(), FloatNear(0.95, 0.01)); +} + +TEST(LoopContractionMitigationModelerTest, IsEnabledFalseResultsInOne) { + LoopContractionMitigationParameters params = { + .is_enabled = false, + .speed_lower_bound = 0, + .speed_upper_bound = 10, + .interpolation_strength_at_speed_lower_bound = 1, + .interpolation_strength_at_speed_upper_bound = 0, + .n_speed_samples = 5}; + + LoopContractionMitigationModeler modeler; + modeler.Reset(params); + + EXPECT_THAT(modeler.Update({3, 4}), FloatNear(1, 0.01)); + EXPECT_THAT(modeler.GetInterpolationValue(), FloatNear(1, 0.01)); +} + +TEST(LoopContractionMitigationModelerTest, ResetClearsModeler) { + LoopContractionMitigationModeler modeler; + modeler.Reset(kDefaultParams); + + EXPECT_THAT(modeler.Update({3, 4}), FloatNear(0.95, 0.01)); + EXPECT_THAT(modeler.GetInterpolationValue(), FloatNear(0.95, 0.01)); + + modeler.Reset(kDefaultParams); + EXPECT_THAT(modeler.GetInterpolationValue(), FloatNear(1, 0.01)); +} + +TEST(LoopContractionMitigationModelerTest, + MultipleUpdatesButLessThanSampleSize) { + LoopContractionMitigationModeler modeler; + modeler.Reset(kDefaultParams); + + // Average is 5. + EXPECT_THAT(modeler.Update({3, 4}), FloatNear(0.95, 0.01)); + EXPECT_THAT(modeler.GetInterpolationValue(), FloatNear(0.95, 0.01)); + + // Average is (5 + 3) / 2=4. + EXPECT_THAT(modeler.Update({0, 3}), FloatNear(0.96, 0.01)); + EXPECT_THAT(modeler.GetInterpolationValue(), FloatNear(0.96, 0.01)); + + // Average is (5 + 3 + 10) / 3 = 6. + EXPECT_THAT(modeler.Update({-10, 0}), FloatNear(0.94, 0.01)); + EXPECT_THAT(modeler.GetInterpolationValue(), FloatNear(0.94, 0.01)); +} + +TEST(LoopContractionMitigationModelerTest, MultipleUpdatesOverSampleSize) { + LoopContractionMitigationModeler modeler; + modeler.Reset(kDefaultParams); + + // Average is 5. + ASSERT_THAT(modeler.Update({0, 5}), FloatNear(0.95, 0.01)); + // Average is (5 + 3) / 2=4. + ASSERT_THAT(modeler.Update({0, 3}), FloatNear(0.96, 0.01)); + // Average is (5 + 3 + 10) / 3 = 6. + ASSERT_THAT(modeler.Update({-10, 0}), FloatNear(0.94, 0.01)); + // Average is (5 + 3 + 10 + 2) / 4 = 5. + ASSERT_THAT(modeler.Update({0, 2}), FloatNear(0.95, 0.01)); + // Average is (5 + 3 + 10 + 2 + 2) / 5 = 4.4. + ASSERT_THAT(modeler.Update({0, 2}), FloatNear(0.956, 0.01)); + + // The next one should clear the first value. + // Average is (3 + 10 + 2 + 2 + 1) / 5 = 3.6) + EXPECT_THAT(modeler.Update({0, 2}), FloatNear(0.964, 0.01)); +} + +TEST(LoopContractionMitigationModelerTest, SaveAndRestore) { + LoopContractionMitigationModeler modeler; + modeler.Reset(kDefaultParams); + + // Average is 5. + ASSERT_THAT(modeler.Update({0, 5}), FloatNear(0.95, 0.01)); + // Average is (5 + 3) / 2=4. + ASSERT_THAT(modeler.Update({0, 3}), FloatNear(0.96, 0.01)); + // Average is (5 + 3 + 10) / 3 = 6. + ASSERT_THAT(modeler.Update({-10, 0}), FloatNear(0.94, 0.01)); + // Average is (5 + 3 + 10 + 2) / 4 = 5. + ASSERT_THAT(modeler.Update({0, 2}), FloatNear(0.95, 0.01)); + // Average is (5 + 3 + 10 + 2 + 2) / 5 = 4.4. + ASSERT_THAT(modeler.Update({0, 2}), FloatNear(0.956, 0.01)); + + modeler.Save(); + + // This clears the first 2 values + // Average is (3 + 10 + 2 + 2 + 1) / 5 = 3.6) + ASSERT_THAT(modeler.Update({0, 2}), FloatNear(0.964, 0.01)); + // Average is (10 + 2 + 2 + 1 + 5) / 5 = 4. + ASSERT_THAT(modeler.Update({0, 5}), FloatNear(0.96, 0.01)); + + modeler.Restore(); + // This should return the last value from before the save. + EXPECT_THAT(modeler.GetInterpolationValue(), FloatNear(0.956, 0.01)); +} + +} // namespace +} // namespace stroke_model +} // namespace ink diff --git a/ink_stroke_modeler/internal/stylus_state_modeler.cc b/ink_stroke_modeler/internal/stylus_state_modeler.cc index b9cefd3..11a9482 100644 --- a/ink_stroke_modeler/internal/stylus_state_modeler.cc +++ b/ink_stroke_modeler/internal/stylus_state_modeler.cc @@ -16,6 +16,7 @@ #include #include +#include #include "ink_stroke_modeler/internal/internal_types.h" #include "ink_stroke_modeler/internal/utils.h" @@ -25,7 +26,8 @@ namespace ink { namespace stroke_model { -void StylusStateModeler::Update(Vec2 position, const StylusState &state) { +void StylusStateModeler::Update(Vec2 position, Time time, + const StylusState &state) { // Possibly NaN should be prohibited in ValidateInput, but due to current // consumers, that can't be tightened for these values currently. if (state.pressure < 0 || std::isnan(state.pressure)) { @@ -39,23 +41,43 @@ void StylusStateModeler::Update(Vec2 position, const StylusState &state) { } if (state_.received_unknown_pressure && state_.received_unknown_tilt && - state_.received_unknown_orientation) { + state_.received_unknown_orientation && + !params_.project_to_segment_along_normal_is_enabled) { // We've stopped tracking all fields, so there's no need to keep updating. - state_.positions_and_stylus_states.clear(); + state_.raw_input_and_stylus_states.clear(); return; } - state_.positions_and_stylus_states.push_back({position, state}); + Vec2 velocity = {0, 0}; + Vec2 acceleration = {0, 0}; + if (!state_.raw_input_and_stylus_states.empty() && + time != state_.raw_input_and_stylus_states.back().time) { + velocity = (position - state_.raw_input_and_stylus_states.back().position) / + (time - state_.raw_input_and_stylus_states.back().time).Value(); + acceleration = + (velocity - state_.raw_input_and_stylus_states.back().velocity) / + (time - state_.raw_input_and_stylus_states.back().time).Value(); + } + + state_.raw_input_and_stylus_states.push_back({ + .position = position, + .velocity = velocity, + .acceleration = acceleration, + .time = time, + .pressure = state.pressure, + .tilt = state.tilt, + .orientation = state.orientation, + }); if (params_.max_input_samples < 0 || - state_.positions_and_stylus_states.size() > + state_.raw_input_and_stylus_states.size() > static_cast(params_.max_input_samples)) { - state_.positions_and_stylus_states.pop_front(); + state_.raw_input_and_stylus_states.pop_front(); } } void StylusStateModeler::Reset(const StylusStateModelerParams ¶ms) { - state_.positions_and_stylus_states.clear(); + state_.raw_input_and_stylus_states.clear(); state_.received_unknown_pressure = false; state_.received_unknown_tilt = false; state_.received_unknown_orientation = false; @@ -63,18 +85,55 @@ void StylusStateModeler::Reset(const StylusStateModelerParams ¶ms) { params_ = params; } -StylusState StylusStateModeler::Query(Vec2 position) const { - if (state_.positions_and_stylus_states.empty()) - return {.pressure = -1, .tilt = -1, .orientation = -1}; +namespace { + +std::optional ProjectToSegmentAlongNormal(Vec2 segment_start, + Vec2 segment_end, + Vec2 position, + Vec2 stroke_normal) { + auto cross = [](Vec2 a, Vec2 b) { return a.x * b.y - a.y * b.x; }; + + Vec2 v = segment_end - segment_start; + float det = cross(stroke_normal, v); + if (det == 0) return std::nullopt; + + Vec2 w = segment_start - position; + float param = cross(w, stroke_normal) / det; + if (param < 0 || param > 1) return std::nullopt; + return param; +} + +} // namespace + +Result StylusStateModeler::Query(Vec2 position, Vec2 stroke_normal, + Time time) const { + if (state_.raw_input_and_stylus_states.empty()) + return { + .position = {0, 0}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = -1, + .tilt = -1, + .orientation = -1, + }; int closest_segment_index = -1; float min_distance = std::numeric_limits::infinity(); float interp_value = 0; - for (decltype(state_.positions_and_stylus_states.size()) i = 0; - i < state_.positions_and_stylus_states.size() - 1; ++i) { - const Vec2 segment_start = state_.positions_and_stylus_states[i].position; - const Vec2 segment_end = state_.positions_and_stylus_states[i + 1].position; - float param = NearestPointOnSegment(segment_start, segment_end, position); + for (decltype(state_.raw_input_and_stylus_states.size()) i = 0; + i < state_.raw_input_and_stylus_states.size() - 1; ++i) { + const Vec2 segment_start = state_.raw_input_and_stylus_states[i].position; + const Vec2 segment_end = state_.raw_input_and_stylus_states[i + 1].position; + float param = 0; + if (params_.project_to_segment_along_normal_is_enabled) { + std::optional temp_param = ProjectToSegmentAlongNormal( + segment_start, segment_end, position, stroke_normal); + if (!temp_param.has_value()) continue; + param = *temp_param; + } else { + param = NearestPointOnSegment(segment_start, segment_end, position); + } float distance = Distance(position, Interp(segment_start, segment_end, param)); if (distance <= min_distance) { @@ -85,18 +144,34 @@ StylusState StylusStateModeler::Query(Vec2 position) const { } if (closest_segment_index < 0) { - const auto &state = state_.positions_and_stylus_states.front().state; - return {.pressure = state_.received_unknown_pressure ? -1 : state.pressure, - .tilt = state_.received_unknown_tilt ? -1 : state.tilt, - .orientation = - state_.received_unknown_orientation ? -1 : state.orientation}; + const auto &state = + Distance(state_.raw_input_and_stylus_states.front().position, + position) < + Distance(state_.raw_input_and_stylus_states.back().position, + position) + ? state_.raw_input_and_stylus_states.front() + : state_.raw_input_and_stylus_states.back(); + return { + .position = state.position, + .velocity = state.velocity, + .acceleration = state.acceleration, + // Jon: keep me straight on this -- state.time or time? + .time = time, + .pressure = state_.received_unknown_pressure ? -1 : state.pressure, + .tilt = state_.received_unknown_tilt ? -1 : state.tilt, + .orientation = + state_.received_unknown_orientation ? -1 : state.orientation, + }; } - auto from_state = - state_.positions_and_stylus_states[closest_segment_index].state; - auto to_state = - state_.positions_and_stylus_states[closest_segment_index + 1].state; - return StylusState{ + auto from_state = state_.raw_input_and_stylus_states[closest_segment_index]; + auto to_state = state_.raw_input_and_stylus_states[closest_segment_index + 1]; + return Result{ + .position = Interp(from_state.position, to_state.position, interp_value), + .velocity = Interp(from_state.velocity, to_state.velocity, interp_value), + .acceleration = + Interp(from_state.acceleration, to_state.acceleration, interp_value), + .time = time, .pressure = state_.received_unknown_pressure ? -1 @@ -107,7 +182,8 @@ StylusState StylusStateModeler::Query(Vec2 position) const { .orientation = state_.received_unknown_orientation ? -1 : InterpAngle(from_state.orientation, - to_state.orientation, interp_value)}; + to_state.orientation, interp_value), + }; } void StylusStateModeler::Save() { @@ -116,7 +192,9 @@ void StylusStateModeler::Save() { } void StylusStateModeler::Restore() { - if (save_active_) state_ = saved_state_; + if (save_active_) { + state_ = saved_state_; + } } } // namespace stroke_model diff --git a/ink_stroke_modeler/internal/stylus_state_modeler.h b/ink_stroke_modeler/internal/stylus_state_modeler.h index 4352794..e6ce44a 100644 --- a/ink_stroke_modeler/internal/stylus_state_modeler.h +++ b/ink_stroke_modeler/internal/stylus_state_modeler.h @@ -18,7 +18,6 @@ #define INK_STROKE_MODELER_INTERNAL_STYLUS_STATE_MODELER_H_ #include -#include #include "ink_stroke_modeler/internal/internal_types.h" #include "ink_stroke_modeler/params.h" @@ -31,12 +30,14 @@ namespace stroke_model { // based on the state of the stylus at the original input points. // // The stylus is modeled by storing the last max_input_samples positions and -// states received via Update(); when queried, it treats the stored positions as -// a polyline, and finds the closest segment. The returned stylus state is a -// linear interpolation between the states associated with the endpoints of the -// segment, correcting angles to account for the "wraparound" that occurs at 0 -// and 2π. The value used for interpolation is based on how far along the -// segment the closest point lies. +// states received via Update() and the velocities and accelerations calculated +// from those positions; when queried, it treats the stored positions as a +// polyline, and finds the relevant segment either by looking at the closest +// segment or by using the `stroke_normal` at that point. The returned stylus +// state is a linear interpolation between the states associated with the +// endpoints of the segment, correcting angles to account for the "wraparound" +// that occurs at 0 and 2π. The value used for interpolation is based on how far +// along the segment the closest point lies. // // If Update() is called with a state in which a field (i.e. pressure, tilt, or // orientation) has a negative value (indicating no information), then the @@ -45,19 +46,24 @@ namespace stroke_model { // pressure and orientation will continue to be interpolated normally. class StylusStateModeler { public: - // Adds a position and state pair to the model. During stroke modeling, these - // values will be taken from the raw input. - void Update(Vec2 position, const StylusState &state); + // Adds a `Result` to the model, calculating the velocity and acceleration + // from the current and previous positions. While it is stored in a `Result` + // type, the values stored don't represent end results. During stroke + // modeling, these values will be taken from the raw input. + void Update(Vec2 position, Time time, const StylusState &state); // Clear the model and reset. void Reset(const StylusStateModelerParams ¶ms); - // Query the model for the state at the given position. During stroke + // Query the model for the `Result` at the given position. During stroke // modeling, the position will be taken from the modeled input. // // If no Update() calls have been received since the last Reset(), this will // return {.pressure = -1, .tilt = -1, .orientation = -1}. - StylusState Query(Vec2 position) const; + // + // `stroke_normal` is only used if + // `project_to_segment_along_normal_is_enabled` is true in the params. + Result Query(Vec2 position, Vec2 stroke_normal, Time time) const; // Saves the current state of the stylus state modeler. See comment on // StrokeModeler::Save() for more details. @@ -68,20 +74,14 @@ class StylusStateModeler { void Restore(); private: - struct PositionAndStylusState { - Vec2 position{0}; - StylusState state; - - PositionAndStylusState(Vec2 position_in, const StylusState &state_in) - : position(position_in), state(state_in) {} - }; - struct ModelerState { bool received_unknown_pressure = false; bool received_unknown_tilt = false; bool received_unknown_orientation = false; - std::deque positions_and_stylus_states; + // This does not actually contain an end results but `Result` has all the + // fields we need to store. + std::deque raw_input_and_stylus_states; }; ModelerState state_; diff --git a/ink_stroke_modeler/internal/stylus_state_modeler_test.cc b/ink_stroke_modeler/internal/stylus_state_modeler_test.cc index a10ca5c..3e92cf3 100644 --- a/ink_stroke_modeler/internal/stylus_state_modeler_test.cc +++ b/ink_stroke_modeler/internal/stylus_state_modeler_test.cc @@ -17,315 +17,792 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" #include "ink_stroke_modeler/internal/internal_types.h" -#include "ink_stroke_modeler/internal/type_matchers.h" #include "ink_stroke_modeler/numbers.h" #include "ink_stroke_modeler/params.h" +#include "ink_stroke_modeler/type_matchers.h" +#include "ink_stroke_modeler/types.h" namespace ink { namespace stroke_model { namespace { constexpr float kTol = 1e-5; -constexpr StylusState kUnknown{.pressure = -1, .tilt = -1, .orientation = -1}; +constexpr StylusState kUnknownState{ + .pressure = -1, .tilt = -1, .orientation = -1}; +const Result kUnknownResult{.position = {0, 0}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = -1, + .tilt = -1, + .orientation = -1}; TEST(StylusStateModelerTest, QueryEmpty) { StylusStateModeler modeler; - EXPECT_EQ(modeler.Query({0, 0}), kUnknown); - EXPECT_EQ(modeler.Query({-5, 3}), kUnknown); + EXPECT_EQ(modeler.Query({0, 0}, {0, 1}, Time(0)), kUnknownResult); + EXPECT_EQ(modeler.Query({-5, 3}, {0, 1}, Time(0.1)), kUnknownResult); } TEST(StylusStateModelerTest, QuerySingleInput) { StylusStateModeler modeler; - modeler.Update({0, 0}, {.pressure = 0.75, .tilt = 0.75, .orientation = 0.75}); - EXPECT_THAT(modeler.Query({0, 0}), - StylusStateNear( - {.pressure = .75, .tilt = .75, .orientation = .75}, kTol)); - EXPECT_THAT(modeler.Query({1, 1}), - StylusStateNear( - {.pressure = .75, .tilt = .75, .orientation = .75}, kTol)); + modeler.Update({0, 0}, Time(0), + {.pressure = 0.75, .tilt = 0.75, .orientation = 0.75}); + EXPECT_THAT(modeler.Query({0, 0}, {0, 1}, Time(0)), + ResultNear( + { + .position = {0, 0}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = .75, + .tilt = .75, + .orientation = .75, + }, + kTol)); + EXPECT_THAT(modeler.Query({1, 1}, {0, 1}, Time(0.1)), + ResultNear({.position = {0, 0}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0.1), + .pressure = .75, + .tilt = .75, + .orientation = .75}, + kTol)); } TEST(StylusStateModelerTest, QueryMultipleInputs) { StylusStateModeler modeler; - modeler.Update({.5, 1.5}, {.pressure = .3, .tilt = .8, .orientation = .1}); - modeler.Update({2, 1.5}, {.pressure = .6, .tilt = .5, .orientation = .7}); - modeler.Update({3, 3.5}, {.pressure = .8, .tilt = .1, .orientation = .3}); - modeler.Update({3.5, 4}, {.pressure = .2, .tilt = .2, .orientation = .2}); - - EXPECT_THAT( - modeler.Query({0, 2}), - StylusStateNear({.pressure = .3, .tilt = .8, .orientation = .1}, kTol)); - EXPECT_THAT( - modeler.Query({1, 2}), - StylusStateNear({.pressure = .4, .tilt = .7, .orientation = .3}, kTol)); - EXPECT_THAT( - modeler.Query({2, 1.5}), - StylusStateNear({.pressure = .6, .tilt = .5, .orientation = .7}, kTol)); - EXPECT_THAT( - modeler.Query({2.5, 1.875}), - StylusStateNear({.pressure = .65, .tilt = .4, .orientation = .6}, kTol)); - EXPECT_THAT( - modeler.Query({2.5, 3.125}), - StylusStateNear({.pressure = .75, .tilt = .2, .orientation = .4}, kTol)); - EXPECT_THAT( - modeler.Query({2.5, 4}), - StylusStateNear({.pressure = .8, .tilt = .1, .orientation = .3}, kTol)); - EXPECT_THAT( - modeler.Query({3, 4}), - StylusStateNear({.pressure = .5, .tilt = .15, .orientation = .25}, kTol)); - EXPECT_THAT( - modeler.Query({4, 4}), - StylusStateNear({.pressure = .2, .tilt = .2, .orientation = .2}, kTol)); + modeler.Update({.5, 1.5}, Time(0), + {.pressure = .3, .tilt = .8, .orientation = .1}); + modeler.Update({2, 1.5}, Time(0.1), + {.pressure = .6, .tilt = .5, .orientation = .7}); + modeler.Update({3, 3.5}, Time(0.2), + {.pressure = .8, .tilt = .1, .orientation = .3}); + modeler.Update({3.5, 4}, Time(0.3), + {.pressure = .2, .tilt = .2, .orientation = .2}); + + EXPECT_THAT(modeler.Query({0, 2}, {0, 1}, Time(0)), + ResultNear( + { + .position = {.5, 1.5}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = .3, + .tilt = .8, + .orientation = .1, + }, + kTol)); + EXPECT_THAT(modeler.Query({1, 2}, {0, -0.5}, Time(0.1)), + ResultNear({.position = {1, 1.5}, + .velocity = {5, 0}, + .acceleration = {50, 0}, + .time = Time(0.1), + .pressure = .4, + .tilt = .7, + .orientation = .3}, + kTol)); + EXPECT_THAT(modeler.Query({2, 1.5}, {2, 2}, Time(0.1)), + ResultNear( + { + .position = {2, 1.5}, + .velocity = {15, 0}, + .acceleration = {150, 0}, + .time = Time(0.1), + .pressure = .6, + .tilt = .5, + .orientation = .7, + }, + kTol)); + EXPECT_THAT(modeler.Query({2.5, 1.875}, {-0.25, 0.125}, Time(0.2)), + ResultNear( + { + .position = {2.25, 2}, + .velocity = {13.75, 5}, + .acceleration = {100, 50}, + .time = Time(0.2), + .pressure = .65, + .tilt = .4, + .orientation = .6, + }, + kTol)); + EXPECT_THAT(modeler.Query({2.5, 3.125}, {0.25, -0.125}, Time(0.22)), + ResultNear( + { + .position = {2.75, 3}, + .velocity = {11.25, 15}, + .acceleration = {0, 150}, + .time = Time(0.22), + .pressure = .75, + .tilt = .2, + .orientation = .4, + }, + kTol)); + EXPECT_THAT(modeler.Query({2.5, 4}, {0.5, -0.5}, Time(0.25)), + ResultNear( + { + .position = {3, 3.5}, + .velocity = {10, 20}, + .acceleration = {-50, 200}, + .time = Time(0.25), + .pressure = .8, + .tilt = .1, + .orientation = .3, + }, + kTol)); + EXPECT_THAT(modeler.Query({3, 4}, {0.25, -0.25}, Time(0.29)), + ResultNear( + { + .position = {3.25, 3.75}, + .velocity = {7.5, 12.5}, + .acceleration = {-50, 25}, + .time = Time(0.29), + .pressure = .5, + .tilt = .15, + .orientation = .25, + }, + kTol)); + EXPECT_THAT(modeler.Query({4, 4}, {0, 1}, Time(0.31)), + ResultNear( + { + .position = {3.5, 4}, + .velocity = {5, 5}, + .acceleration = {-50, -150}, + .time = Time(0.31), + .pressure = .2, + .tilt = .2, + .orientation = .2, + }, + kTol)); } TEST(StylusStateModelerTest, QueryStaleInputsAreDiscarded) { StylusStateModeler modeler; - modeler.Update({1, 1}, {.pressure = .6, .tilt = .5, .orientation = .4}); - modeler.Update({-1, 2}, {.pressure = .3, .tilt = .7, .orientation = .6}); - modeler.Update({-4, 0}, {.pressure = .9, .tilt = .7, .orientation = .3}); - modeler.Update({-6, -3}, {.pressure = .4, .tilt = .3, .orientation = .5}); - modeler.Update({-5, -5}, {.pressure = .3, .tilt = .3, .orientation = .1}); - modeler.Update({-3, -4}, {.pressure = .6, .tilt = .8, .orientation = .3}); - modeler.Update({-6, -7}, {.pressure = .9, .tilt = .8, .orientation = .1}); - modeler.Update({-9, -8}, {.pressure = .8, .tilt = .2, .orientation = .2}); - modeler.Update({-11, -5}, {.pressure = .2, .tilt = .4, .orientation = .7}); - modeler.Update({-10, -2}, {.pressure = .7, .tilt = .3, .orientation = .2}); - - EXPECT_THAT( - modeler.Query({2, 0}), - StylusStateNear({.pressure = .6, .tilt = .5, .orientation = .4}, kTol)); - EXPECT_THAT( - modeler.Query({1, 3.5}), - StylusStateNear({.pressure = .45, .tilt = .6, .orientation = .5}, kTol)); - EXPECT_THAT( - modeler.Query({-3, 17. / 6}), - StylusStateNear({.pressure = .5, .tilt = .7, .orientation = .5}, kTol)); + modeler.Update({1, 1}, Time(0), + {.pressure = .6, .tilt = .5, .orientation = .4}); + modeler.Update({-1, 2}, Time(0.1), + {.pressure = .3, .tilt = .7, .orientation = .6}); + modeler.Update({-4, 0}, Time(0.2), + {.pressure = .9, .tilt = .7, .orientation = .3}); + modeler.Update({-6, -3}, Time(0.3), + {.pressure = .4, .tilt = .3, .orientation = .5}); + modeler.Update({-5, -5}, Time(0.4), + {.pressure = .3, .tilt = .3, .orientation = .1}); + modeler.Update({-3, -4}, Time(0.5), + {.pressure = .6, .tilt = .8, .orientation = .3}); + modeler.Update({-6, -7}, Time(0.6), + {.pressure = .9, .tilt = .8, .orientation = .1}); + modeler.Update({-9, -8}, Time(0.7), + {.pressure = .8, .tilt = .2, .orientation = .2}); + modeler.Update({-11, -5}, Time(0.8), + {.pressure = .2, .tilt = .4, .orientation = .7}); + modeler.Update({-10, -2}, Time(0.9), + {.pressure = .7, .tilt = .3, .orientation = .2}); + + EXPECT_THAT(modeler.Query({2, 0}, {0, 1}, Time(0)), + ResultNear( + { + .position = {1, 1}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = .6, + .tilt = .5, + .orientation = .4, + }, + kTol)); + EXPECT_THAT(modeler.Query({1, 3.5}, {-1, -2}, Time(0.1)), + ResultNear( + { + .position = {0, 1.5}, + .velocity = {-10, 5}, + .acceleration = {-100, 50}, + .time = Time(0.1), + .pressure = .45, + .tilt = .6, + .orientation = .5, + }, + kTol)); + EXPECT_THAT(modeler.Query({-3, 17. / 6}, {1, -1.5}, Time(0.2)), + ResultNear( + { + .position = {-2, 4. / 3.}, + .velocity = {-70. / 3., 0}, + .acceleration = {-166.666656, -33.33334}, + .time = Time(0.2), + .pressure = .5, + .tilt = .7, + .orientation = .5, + }, + kTol)); // This causes the point at {1, 1} to be discarded. - modeler.Update({-8, 0}, {.pressure = .6, .tilt = .8, .orientation = .9}); - EXPECT_THAT( - modeler.Query({2, 0}), - StylusStateNear({.pressure = .3, .tilt = .7, .orientation = .6}, kTol)); - EXPECT_THAT( - modeler.Query({1, 3.5}), - StylusStateNear({.pressure = .3, .tilt = .7, .orientation = .6}, kTol)); - EXPECT_THAT( - modeler.Query({-3, 17. / 6}), - StylusStateNear({.pressure = .5, .tilt = .7, .orientation = .5}, kTol)); + modeler.Update({-8, 0}, Time(1), + {.pressure = .6, .tilt = .8, .orientation = .9}); + EXPECT_THAT(modeler.Query({2, 0}, {0, 1}, Time(0.1)), + ResultNear( + { + .position = {-1, 2}, + .velocity = {-20, 10}, + .acceleration = {-200, 100}, + .time = Time(0.1), + .pressure = .3, + .tilt = .7, + .orientation = .6, + }, + kTol)); + EXPECT_THAT(modeler.Query({1, 3.5}, {0, 1}, Time(0.2)), + ResultNear( + { + .position = {-1, 2}, + .velocity = {-20, 10}, + .acceleration = {-200, 100}, + .time = Time(0.2), + .pressure = .3, + .tilt = .7, + .orientation = .6, + }, + kTol)); + EXPECT_THAT(modeler.Query({-3, 17. / 6}, {1, -1.5}, Time(0.3)), + ResultNear( + { + .position = {-2, 4. / 3.}, + .velocity = {-70. / 3., 0}, + .acceleration = {-166.666656, -33.33334}, + .time = Time(0.3), + .pressure = .5, + .tilt = .7, + .orientation = .5, + }, + kTol)); // This causes the point at {-1, 2} to be discarded. - modeler.Update({-8, 0}, {.pressure = .6, .tilt = .8, .orientation = .9}); - EXPECT_THAT( - modeler.Query({2, 0}), - StylusStateNear({.pressure = .9, .tilt = .7, .orientation = .3}, kTol)); - EXPECT_THAT( - modeler.Query({1, 3.5}), - StylusStateNear({.pressure = .9, .tilt = .7, .orientation = .3}, kTol)); - EXPECT_THAT( - modeler.Query({-3, 17. / 6}), - StylusStateNear({.pressure = .9, .tilt = .7, .orientation = .3}, kTol)); + modeler.Update({-8, 0}, Time(1.1), + {.pressure = .6, .tilt = .8, .orientation = .9}); + EXPECT_THAT(modeler.Query({2, 0}, {0, 1}, Time(0.3)), + ResultNear( + { + .position = {-4, 0}, + .velocity = {-30, -20}, + .acceleration = {-100, -300}, + .time = Time(0.3), + .pressure = .9, + .tilt = .7, + .orientation = .3, + }, + kTol)); + EXPECT_THAT(modeler.Query({1, 3.5}, {0, 1}, Time(0.4)), + ResultNear( + { + .position = {-4, 0}, + .velocity = {-30, -20}, + .acceleration = {-100, -300}, + .time = Time(0.4), + .pressure = .9, + .tilt = .7, + .orientation = .3, + }, + kTol)); + EXPECT_THAT(modeler.Query({-3, 17. / 6}, {-6, 2}, Time(0.5)), + ResultNear( + { + .position = {-4, 0}, + .velocity = {-30, -20}, + .acceleration = {-100, -300}, + .time = Time(0.5), + .pressure = .9, + .tilt = .7, + .orientation = .3, + }, + kTol)); } TEST(StylusStateModelerTest, QueryCyclicOrientationInterpolation) { StylusStateModeler modeler; - modeler.Update({0, 0}, {.pressure = 0, .tilt = 0, .orientation = 1.8 * kPi}); - modeler.Update({0, 1}, {.pressure = 0, .tilt = 0, .orientation = .2 * kPi}); - modeler.Update({0, 2}, {.pressure = 0, .tilt = 0, .orientation = 1.6 * kPi}); - - EXPECT_NEAR(modeler.Query({0, .25}).orientation, 1.9 * kPi, 1e-5); - EXPECT_NEAR(modeler.Query({0, .75}).orientation, .1 * kPi, 1e-5); - EXPECT_NEAR(modeler.Query({0, 1.25}).orientation, .05 * kPi, 1e-5); - EXPECT_NEAR(modeler.Query({0, 1.75}).orientation, 1.75 * kPi, 1e-5); + modeler.Update({0, 0}, Time(0), + {.pressure = 0, .tilt = 0, .orientation = 1.8 * kPi}); + modeler.Update({0, 1}, Time(1), + {.pressure = 0, .tilt = 0, .orientation = .2 * kPi}); + modeler.Update({0, 2}, Time(2), + {.pressure = 0, .tilt = 0, .orientation = 1.6 * kPi}); + + EXPECT_NEAR(modeler.Query({0, .25}, {1, 0}, Time(0)).orientation, 1.9 * kPi, + kTol); + EXPECT_NEAR(modeler.Query({0, .75}, {1, 0}, Time(1)).orientation, .1 * kPi, + kTol); + EXPECT_NEAR(modeler.Query({0, 1.25}, {1, 0}, Time(1.5)).orientation, + .05 * kPi, kTol); + EXPECT_NEAR(modeler.Query({0, 1.75}, {1, 0}, Time(2)).orientation, 1.75 * kPi, + kTol); } TEST(StylusStateModelerTest, QueryAndReset) { StylusStateModeler modeler; - modeler.Update({4, 5}, {.pressure = .4, .tilt = .9, .orientation = .1}); - modeler.Update({7, 8}, {.pressure = .1, .tilt = .2, .orientation = .5}); - EXPECT_THAT( - modeler.Query({10, 12}), - StylusStateNear({.pressure = .1, .tilt = .2, .orientation = .5}, kTol)); + modeler.Update({4, 5}, Time(0), + {.pressure = .4, .tilt = .9, .orientation = .1}); + modeler.Update({7, 8}, Time(1), + {.pressure = .1, .tilt = .2, .orientation = .5}); + EXPECT_THAT(modeler.Query({10, 12}, {0.5, -0.5}, Time(0)), + ResultNear( + { + .position = {7, 8}, + .velocity = {3, 3}, + .acceleration = {3, 3}, + .time = Time(0), + .pressure = .1, + .tilt = .2, + .orientation = .5, + }, + kTol)); modeler.Reset(StylusStateModelerParams{}); - EXPECT_EQ(modeler.Query({10, 12}), kUnknown); - - modeler.Update({-1, 4}, {.pressure = .4, .tilt = .6, .orientation = .8}); - EXPECT_THAT( - modeler.Query({6, 7}), - StylusStateNear({.pressure = .4, .tilt = .6, .orientation = .8}, kTol)); - - modeler.Update({-3, 0}, {.pressure = .7, .tilt = .2, .orientation = .5}); - EXPECT_THAT( - modeler.Query({-2, 2}), - StylusStateNear({.pressure = .55, .tilt = .4, .orientation = .65}, kTol)); - EXPECT_THAT( - modeler.Query({0, 5}), - StylusStateNear({.pressure = .4, .tilt = .6, .orientation = .8}, kTol)); + EXPECT_EQ(modeler.Query({10, 12}, {0.5, -0.5}, Time(0)), kUnknownResult); + + modeler.Update({-1, 4}, Time(2), + {.pressure = .4, .tilt = .6, .orientation = .8}); + EXPECT_THAT(modeler.Query({6, 7}, {-7, -3}, Time(2)), + ResultNear( + { + .position = {-1, 4}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(2), + .pressure = .4, + .tilt = .6, + .orientation = .8, + }, + kTol)); + + modeler.Update({-3, 0}, Time(3), + {.pressure = .7, .tilt = .2, .orientation = .5}); + EXPECT_THAT(modeler.Query({-2, 2}, {0, 1}, Time(2.5)), + ResultNear( + { + .position = {-2, 2}, + .velocity = {-1, -2}, + .acceleration = {-1, -2}, + .time = Time(2.5), + .pressure = .55, + .tilt = .4, + .orientation = .65, + }, + kTol)); + EXPECT_THAT(modeler.Query({0, 5}, {-0.4, 0.2}, Time(0)), + ResultNear( + { + .position = {-1, 4}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = .4, + .tilt = .6, + .orientation = .8, + }, + kTol)); } TEST(StylusStateModelerTest, UpdateWithUnknownState) { StylusStateModeler modeler; - modeler.Update({1, 2}, {.pressure = .1, .tilt = .2, .orientation = .3}); - modeler.Update({2, 3}, {.pressure = .3, .tilt = .4, .orientation = .5}); - EXPECT_THAT( - modeler.Query({2, 2}), - StylusStateNear({.pressure = .2, .tilt = .3, .orientation = .4}, kTol)); + modeler.Update({1, 2}, Time(0), + {.pressure = .1, .tilt = .2, .orientation = .3}); + modeler.Update({2, 3}, Time(1), + {.pressure = .3, .tilt = .4, .orientation = .5}); + EXPECT_THAT(modeler.Query({2, 2}, {-0.5, 0.5}, Time(0)), + ResultNear({.position = {1.5, 2.5}, + .velocity = {0.5, 0.5}, + .acceleration = {0.5, 0.5}, + .time = Time(0), + .pressure = .2, + .tilt = .3, + .orientation = .4}, + kTol)); + + modeler.Update({5, 5}, Time(2), kUnknownState); + EXPECT_EQ(modeler.Query({5, 5}, {-0.5, 0.5}, Time(1)), kUnknownResult); + + modeler.Update({2, 3}, Time(3), + {.pressure = .3, .tilt = .4, .orientation = .5}); + EXPECT_EQ(modeler.Query({1, 2}, {-0.5, 0.5}, Time(2)), kUnknownResult); + + modeler.Update({-1, 3}, Time(4), kUnknownState); + EXPECT_EQ(modeler.Query({7, 9}, {-0.5, 0.5}, Time(3)), kUnknownResult); - modeler.Update({5, 5}, kUnknown); - EXPECT_EQ(modeler.Query({5, 5}), kUnknown); - - modeler.Update({2, 3}, {.pressure = .3, .tilt = .4, .orientation = .5}); - EXPECT_EQ(modeler.Query({1, 2}), kUnknown); + modeler.Reset(StylusStateModelerParams{}); + modeler.Update({3, 3}, Time(5), + {.pressure = .7, .tilt = .6, .orientation = .5}); + EXPECT_THAT(modeler.Query({3, 3}, {-0.5, 0.5}, Time(0.2)), + ResultNear({.position = {3, 3}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0.2), + .pressure = .7, + .tilt = .6, + .orientation = .5}, + kTol)); +} - modeler.Update({-1, 3}, kUnknown); - EXPECT_EQ(modeler.Query({7, 9}), kUnknown); +TEST(StylusStateModelerTest, StrokeNormalIgnored) { + StylusStateModeler modeler; - modeler.Reset(StylusStateModelerParams{}); - modeler.Update({3, 3}, {.pressure = .7, .tilt = .6, .orientation = .5}); - EXPECT_THAT( - modeler.Query({3, 3}), - StylusStateNear({.pressure = .7, .tilt = .6, .orientation = .5}, kTol)); + modeler.Update({4, 5}, Time(0), + {.pressure = .4, .tilt = .9, .orientation = .1}); + modeler.Update({7, 8}, Time(1), + {.pressure = .1, .tilt = .2, .orientation = .5}); + EXPECT_THAT(modeler.Query({5, 7}, {0.5, -0.5}, Time(0.2)), + ResultNear(modeler.Query({5, 7}, {0, 1}, Time(0.2)), kTol)); } TEST(StylusStateModelerTest, ModelPressureOnly) { StylusStateModeler modeler; - modeler.Update({0, 0}, {.pressure = .5, .tilt = -2, .orientation = -.1}); - EXPECT_THAT( - modeler.Query({1, 1}), - StylusStateNear({.pressure = .5, .tilt = -1, .orientation = -1}, kTol)); - - modeler.Update({2, 0}, {.pressure = .7, .tilt = -2, .orientation = -.1}); - EXPECT_THAT( - modeler.Query({1, 1}), - StylusStateNear({.pressure = .6, .tilt = -1, .orientation = -1}, kTol)); + modeler.Update({0, 0}, Time(0), + {.pressure = .5, .tilt = -2, .orientation = -.1}); + EXPECT_THAT(modeler.Query({1, 1}, {0, 1}, Time(0)), + ResultNear( + { + .position = {0, 0}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = .5, + .tilt = -1, + .orientation = -1, + }, + kTol)); + + modeler.Update({2, 0}, Time(1), + {.pressure = .7, .tilt = -2, .orientation = -.1}); + EXPECT_THAT(modeler.Query({1, 1}, {0, -1}, Time(1)), + ResultNear( + { + .position = {1, 0}, + .velocity = {1, 0}, + .acceleration = {1, 0}, + .time = Time(1), + .pressure = .6, + .tilt = -1, + .orientation = -1, + }, + kTol)); } TEST(StylusStateModelerTest, ModelTiltOnly) { StylusStateModeler modeler; - modeler.Update({0, 0}, {.pressure = -2, .tilt = .5, .orientation = -.1}); - EXPECT_THAT( - modeler.Query({1, 1}), - StylusStateNear({.pressure = -1, .tilt = .5, .orientation = -1}, kTol)); - - modeler.Update({2, 0}, {.pressure = -2, .tilt = .3, .orientation = -.1}); - EXPECT_THAT( - modeler.Query({1, 1}), - StylusStateNear({.pressure = -1, .tilt = .4, .orientation = -1}, kTol)); + modeler.Update({0, 0}, Time(0), + {.pressure = -2, .tilt = .5, .orientation = -.1}); + EXPECT_THAT(modeler.Query({1, 1}, {0, 1}, Time(0)), + ResultNear( + { + .position = {0, 0}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = -1, + .tilt = .5, + .orientation = -1, + }, + kTol)); + + modeler.Update({2, 0}, Time(1), + {.pressure = -2, .tilt = .3, .orientation = -.1}); + EXPECT_THAT(modeler.Query({1, 1}, {0, -1}, Time(1)), + ResultNear( + { + .position = {1, 0}, + .velocity = {1, 0}, + .acceleration = {1, 0}, + .time = Time(1), + .pressure = -1, + .tilt = .4, + .orientation = -1, + }, + kTol)); } TEST(StylusStateModelerTest, ModelOrientationOnly) { StylusStateModeler modeler; - modeler.Update({0, 0}, {.pressure = -2, .tilt = -.1, .orientation = 1}); - EXPECT_THAT( - modeler.Query({1, 1}), - StylusStateNear({.pressure = -1, .tilt = -1, .orientation = 1}, kTol)); - - modeler.Update({2, 0}, {.pressure = -2, .tilt = -.3, .orientation = 2}); - EXPECT_THAT( - modeler.Query({1, 1}), - StylusStateNear({.pressure = -1, .tilt = -1, .orientation = 1.5}, kTol)); + modeler.Update({0, 0}, Time(0), + {.pressure = -2, .tilt = -.1, .orientation = 1}); + EXPECT_THAT(modeler.Query({1, 1}, {0, 1}, Time(0)), + ResultNear( + { + .position = {0, 0}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = -1, + .tilt = -1, + .orientation = 1, + }, + kTol)); + + modeler.Update({2, 0}, Time(1), + {.pressure = -2, .tilt = -.3, .orientation = 2}); + EXPECT_THAT(modeler.Query({1, 1}, {0, -1}, Time(1)), + ResultNear( + { + .position = {1, 0}, + .velocity = {1, 0}, + .acceleration = {1, 0}, + .time = Time(1), + .pressure = -1, + .tilt = -1, + .orientation = 1.5, + }, + kTol)); } TEST(StylusStateModelerTest, DropFieldsOneByOne) { StylusStateModeler modeler; - modeler.Update({0, 0}, {.pressure = .5, .tilt = .5, .orientation = .5}); - EXPECT_THAT( - modeler.Query({1, 0}), - StylusStateNear({.pressure = .5, .tilt = .5, .orientation = .5}, kTol)); - - modeler.Update({2, 0}, {.pressure = .3, .tilt = .7, .orientation = -1}); - EXPECT_THAT( - modeler.Query({1, 0}), - StylusStateNear({.pressure = .4, .tilt = .6, .orientation = -1}, kTol)); - - modeler.Update({4, 0}, {.pressure = .1, .tilt = -1, .orientation = 1}); - EXPECT_THAT( - modeler.Query({3, 0}), - StylusStateNear({.pressure = .2, .tilt = -1, .orientation = -1}, kTol)); - - modeler.Update({6, 0}, {.pressure = -1, .tilt = .2, .orientation = 0}); - EXPECT_THAT(modeler.Query({5, 0}), StylusStateNear(kUnknown, kTol)); - - modeler.Update({8, 0}, {.pressure = .3, .tilt = .4, .orientation = .5}); - EXPECT_THAT(modeler.Query({7, 0}), StylusStateNear(kUnknown, kTol)); + modeler.Update({0, 0}, Time(0), + {.pressure = .5, .tilt = .5, .orientation = .5}); + EXPECT_THAT(modeler.Query({1, 0}, {0, 1}, Time(0)), + ResultNear( + { + .position = {0, 0}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = .5, + .tilt = .5, + .orientation = .5, + }, + kTol)); + + modeler.Update({2, 0}, Time(1), + {.pressure = .3, .tilt = .7, .orientation = -1}); + EXPECT_THAT(modeler.Query({1, 0}, {0, 1}, Time(1)), + ResultNear( + { + .position = {1, 0}, + .velocity = {1, 0}, + .acceleration = {1, 0}, + .time = Time(1), + .pressure = .4, + .tilt = .6, + .orientation = -1, + }, + kTol)); + + modeler.Update({4, 0}, Time(2), + {.pressure = .1, .tilt = -1, .orientation = 1}); + EXPECT_THAT(modeler.Query({3, 0}, {0, 1}, Time(2)), + ResultNear( + { + .position = {3, 0}, + .velocity = {2, 0}, + .acceleration = {1, 0}, + .time = Time(2), + .pressure = .2, + .tilt = -1, + .orientation = -1, + }, + kTol)); + + modeler.Update({6, 0}, Time(3), + {.pressure = -1, .tilt = .2, .orientation = 0}); + EXPECT_THAT(modeler.Query({5, 0}, {0, 1}, Time(3)), + ResultNear( + { + .position = {0, 0}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = -1, + .tilt = -1, + .orientation = -1, + }, + kTol)); + + modeler.Update({8, 0}, Time(4), + {.pressure = .3, .tilt = .4, .orientation = .5}); + EXPECT_THAT(modeler.Query({7, 0}, {0, 1}, Time(4)), + ResultNear( + { + .position = {0, 0}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = -1, + .tilt = -1, + .orientation = -1, + }, + kTol)); modeler.Reset(StylusStateModelerParams{}); - EXPECT_THAT(modeler.Query({1, 0}), StylusStateNear(kUnknown, kTol)); - - modeler.Update({0, 0}, {.pressure = .1, .tilt = .8, .orientation = .3}); - EXPECT_THAT( - modeler.Query({1, 0}), - StylusStateNear({.pressure = .1, .tilt = .8, .orientation = .3}, kTol)); + EXPECT_THAT(modeler.Query({1, 0}, {0, 1}, Time(0)), + ResultNear(kUnknownResult, kTol)); + + modeler.Update({0, 0}, Time(5), + {.pressure = .1, .tilt = .8, .orientation = .3}); + EXPECT_THAT(modeler.Query({1, 0}, {0, 1}, Time(5)), + ResultNear( + { + .position = {0, 0}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(5), + .pressure = .1, + .tilt = .8, + .orientation = .3, + }, + kTol)); } TEST(StylusStateModelerTest, SaveAndRestore) { StylusStateModeler modeler; - modeler.Update({1, 1}, {.pressure = .6, .tilt = .5, .orientation = .4}); - modeler.Update({-1, 2}, {.pressure = .3, .tilt = .7, .orientation = .6}); - modeler.Update({-4, 0}, {.pressure = .9, .tilt = .7, .orientation = .3}); - modeler.Update({-6, -3}, {.pressure = .4, .tilt = .3, .orientation = .5}); - modeler.Update({-5, -5}, {.pressure = .3, .tilt = .3, .orientation = .1}); - modeler.Update({-3, -4}, {.pressure = .6, .tilt = .8, .orientation = .3}); - modeler.Update({-6, -7}, {.pressure = .9, .tilt = .8, .orientation = .1}); - modeler.Update({-9, -8}, {.pressure = .8, .tilt = .2, .orientation = .2}); - modeler.Update({-11, -5}, {.pressure = .2, .tilt = .4, .orientation = .7}); - modeler.Update({-10, -2}, {.pressure = .7, .tilt = .3, .orientation = .2}); - - ASSERT_THAT( - modeler.Query({2, 0}), - StylusStateNear({.pressure = .6, .tilt = .5, .orientation = .4}, kTol)); + modeler.Update({1, 1}, Time(0), + {.pressure = .6, .tilt = .5, .orientation = .4}); + modeler.Update({-1, 2}, Time(1), + {.pressure = .3, .tilt = .7, .orientation = .6}); + modeler.Update({-4, 0}, Time(2), + {.pressure = .9, .tilt = .7, .orientation = .3}); + modeler.Update({-6, -3}, Time(3), + {.pressure = .4, .tilt = .3, .orientation = .5}); + modeler.Update({-5, -5}, Time(4), + {.pressure = .3, .tilt = .3, .orientation = .1}); + modeler.Update({-3, -4}, Time(5), + {.pressure = .6, .tilt = .8, .orientation = .3}); + modeler.Update({-6, -7}, Time(6), + {.pressure = .9, .tilt = .8, .orientation = .1}); + modeler.Update({-9, -8}, Time(7), + {.pressure = .8, .tilt = .2, .orientation = .2}); + modeler.Update({-11, -5}, Time(8), + {.pressure = .2, .tilt = .4, .orientation = .7}); + modeler.Update({-10, -2}, Time(9), + {.pressure = .7, .tilt = .3, .orientation = .2}); + + ASSERT_THAT(modeler.Query({2, 0}, {0, 1}, Time(0)), + ResultNear( + { + .position = {1, 1}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = .6, + .tilt = .5, + .orientation = .4, + }, + kTol)); // Calling restore with no save should have no effect. modeler.Restore(); - EXPECT_THAT( - modeler.Query({2, 0}), - StylusStateNear({.pressure = .6, .tilt = .5, .orientation = .4}, kTol)); + EXPECT_THAT(modeler.Query({2, 0}, {0, 1}, Time(0)), + ResultNear( + { + .position = {1, 1}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = .6, + .tilt = .5, + .orientation = .4, + }, + kTol)); modeler.Save(); // This causes the points at {1, 1} and {-1, 2} to be discarded. - modeler.Update({-8, 0}, {.pressure = .6, .tilt = .8, .orientation = .9}); - modeler.Update({-8, 0}, {.pressure = .6, .tilt = .8, .orientation = .9}); - EXPECT_THAT( - modeler.Query({2, 0}), - StylusStateNear({.pressure = .9, .tilt = .7, .orientation = .3}, kTol)); + modeler.Update({-8, 0}, Time(10), + {.pressure = .6, .tilt = .8, .orientation = .9}); + modeler.Update({-8, 0}, Time(11), + {.pressure = .6, .tilt = .8, .orientation = .9}); + EXPECT_THAT(modeler.Query({2, 0}, {0, 1}, Time(0)), + ResultNear( + { + .position = {-4, 0}, + .velocity = {-3, -2}, + .acceleration = {-1, -3}, + .time = Time(0), + .pressure = .9, + .tilt = .7, + .orientation = .3, + }, + kTol)); // Restoring should revert the updates. modeler.Restore(); - EXPECT_THAT( - modeler.Query({2, 0}), - StylusStateNear({.pressure = .6, .tilt = .5, .orientation = .4}, kTol)); + EXPECT_THAT(modeler.Query({2, 0}, {0, 1}, Time(0)), + ResultNear( + { + .position = {1, 1}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = .6, + .tilt = .5, + .orientation = .4, + }, + kTol)); // Restoring should not have cleared the saved state, so we can repeat the // action. - modeler.Update({-8, 0}, {.pressure = .6, .tilt = .8, .orientation = .9}); - modeler.Update({-8, 0}, {.pressure = .6, .tilt = .8, .orientation = .9}); - EXPECT_THAT( - modeler.Query({2, 0}), - StylusStateNear({.pressure = .9, .tilt = .7, .orientation = .3}, kTol)); + modeler.Update({-8, 0}, Time(12), + {.pressure = .6, .tilt = .8, .orientation = .9}); + modeler.Update({-8, 0}, Time(13), + {.pressure = .6, .tilt = .8, .orientation = .9}); + EXPECT_THAT(modeler.Query({2, 0}, {0, 1}, Time(0)), + ResultNear( + { + .position = {-4, 0}, + .velocity = {-3, -2}, + .acceleration = {-1, -3}, + .time = Time(0), + .pressure = .9, + .tilt = .7, + .orientation = .3, + }, + kTol)); modeler.Restore(); - EXPECT_THAT( - modeler.Query({2, 0}), - StylusStateNear({.pressure = .6, .tilt = .5, .orientation = .4}, kTol)); + EXPECT_THAT(modeler.Query({2, 0}, {0, 1}, Time(0)), + ResultNear( + { + .position = {1, 1}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = .6, + .tilt = .5, + .orientation = .4, + }, + kTol)); // Calling Reset should clear the save point so that calling Restore should // have no effect. modeler.Reset(StylusStateModelerParams{}); - modeler.Update({-1, 4}, {.pressure = .4, .tilt = .6, .orientation = .8}); - EXPECT_THAT( - modeler.Query({6, 7}), - StylusStateNear({.pressure = .4, .tilt = .6, .orientation = .8}, kTol)); + modeler.Update({-1, 4}, Time(14), + {.pressure = .4, .tilt = .6, .orientation = .8}); + EXPECT_THAT(modeler.Query({6, 7}, {0, 1}, Time(0)), + ResultNear( + { + .position = {-1, 4}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = .4, + .tilt = .6, + .orientation = .8, + }, + kTol)); modeler.Restore(); - EXPECT_THAT( - modeler.Query({6, 7}), - StylusStateNear({.pressure = .4, .tilt = .6, .orientation = .8}, kTol)); + EXPECT_THAT(modeler.Query({6, 7}, {0, 1}, Time(0)), + ResultNear( + { + .position = {-1, 4}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = .4, + .tilt = .6, + .orientation = .8, + }, + kTol)); } } // namespace diff --git a/ink_stroke_modeler/internal/stylus_state_modeler_with_new_projection_test.cc b/ink_stroke_modeler/internal/stylus_state_modeler_with_new_projection_test.cc new file mode 100644 index 0000000..448591f --- /dev/null +++ b/ink_stroke_modeler/internal/stylus_state_modeler_with_new_projection_test.cc @@ -0,0 +1,750 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "ink_stroke_modeler/internal/stylus_state_modeler.h" +#include "ink_stroke_modeler/numbers.h" +#include "ink_stroke_modeler/params.h" +#include "ink_stroke_modeler/type_matchers.h" +#include "ink_stroke_modeler/types.h" + +namespace ink { +namespace stroke_model { +namespace { + +constexpr float kTol = 1e-5; +const Result kUnknownResult{.position = {0, 0}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = -1, + .tilt = -1, + .orientation = -1}; + +constexpr StylusStateModelerParams kDefaultParams{ + .max_input_samples = 10, + .project_to_segment_along_normal_is_enabled = true, +}; + +TEST(StylusStateModelerWithNewProjectionTest, QueryEmpty) { + StylusStateModeler modeler; + modeler.Reset(kDefaultParams); + EXPECT_EQ(modeler.Query({0, 0}, {0, 1}, Time(0)), kUnknownResult); + EXPECT_EQ(modeler.Query({-5, 3}, {0, 1}, Time(0.1)), kUnknownResult); +} + +TEST(StylusStateModelerWithNewProjectionTest, QuerySingleInput) { + StylusStateModeler modeler; + modeler.Reset(kDefaultParams); + modeler.Update({0, 0}, Time(0), + {.pressure = 0.75, .tilt = 0.75, .orientation = 0.75}); + EXPECT_THAT(modeler.Query({0, 0}, {0, 1.1}, Time(0)), + ResultNear( + { + .position = {0, 0}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = .75, + .tilt = .75, + .orientation = .75, + }, + kTol)); + EXPECT_THAT(modeler.Query({1, 1}, {0, 1.1}, Time(0.1)), + ResultNear({.position = {0, 0}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0.1), + .pressure = .75, + .tilt = .75, + .orientation = .75}, + kTol)); +} + +TEST(StylusStateModelerWithNewProjectionTest, QueryMultipleInputs) { + StylusStateModeler modeler; + modeler.Reset(kDefaultParams); + modeler.Update({.5, 1.5}, Time(0), + {.pressure = .3, .tilt = .8, .orientation = .1}); + modeler.Update({2, 1.5}, Time(0.1), + {.pressure = .6, .tilt = .5, .orientation = .7}); + modeler.Update({3, 3.5}, Time(0.2), + {.pressure = .8, .tilt = .1, .orientation = .3}); + modeler.Update({3.5, 4}, Time(0.3), + {.pressure = .2, .tilt = .2, .orientation = .2}); + + EXPECT_THAT(modeler.Query({0, 2}, {0, 1.1}, Time(0)), + ResultNear( + { + .position = {.5, 1.5}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = .3, + .tilt = .8, + .orientation = .1, + }, + kTol)); + EXPECT_THAT(modeler.Query({1, 2}, {0, -0.55}, Time(0.1)), + ResultNear({.position = {1, 1.5}, + .velocity = {5, 0}, + .acceleration = {50, 0}, + .time = Time(0.1), + .pressure = .4, + .tilt = .7, + .orientation = .3}, + kTol)); + EXPECT_THAT(modeler.Query({2, 1.5}, {2, 2.1}, Time(0.1)), + ResultNear( + { + .position = {2, 1.5}, + .velocity = {15, 0}, + .acceleration = {150, 0}, + .time = Time(0.1), + .pressure = .6, + .tilt = .5, + .orientation = .7, + }, + kTol)); + EXPECT_THAT(modeler.Query({2.5, 1.875}, {-0.3, 0.125}, Time(0.2)), + ResultNear({.position = {2.24138, 1.98276}, + .velocity = {13.79310, 4.82759}, + .acceleration = {101.72414, 48.27586}, + .time = Time(0.2), + .pressure = 0.648276, + .tilt = 0.403448, + .orientation = 0.603448}, + kTol)); + EXPECT_THAT(modeler.Query({2.5, 3.125}, {0.3, -0.125}, Time(0.22)), + ResultNear({.position = {2.75862, 3.01724}, + .velocity = {11.20690, 15.17241}, + .acceleration = {-1.72414, 151.72414}, + .time = Time(0.22), + .pressure = 0.751724, + .tilt = 0.196552, + .orientation = 0.396552}, + kTol)); + EXPECT_THAT(modeler.Query({2.5, 4}, {0.5, -0.55}, Time(0.25)), + ResultNear({.position = {2.98387, 3.46774}, + .velocity = {10.08064, 19.67742}, + .acceleration = {-46.77420, 196.77420}, + .time = Time(0.25), + .pressure = 0.796774, + .tilt = 0.106452, + .orientation = 0.306452}, + kTol)); + EXPECT_THAT(modeler.Query({3, 4}, {0.3, -0.25}, Time(0.29)), + ResultNear({.position = {3.27273, 3.77273}, + .velocity = {7.27273, 11.81818}, + .acceleration = {-50.00000, 9.09090}, + .time = Time(0.29), + .pressure = 0.472727, + .tilt = 0.154545, + .orientation = 0.245455}, + kTol)); + EXPECT_THAT(modeler.Query({4, 4}, {0, 1.1}, Time(0.31)), + ResultNear( + { + .position = {3.5, 4}, + .velocity = {5, 5}, + .acceleration = {-50, -150}, + .time = Time(0.31), + .pressure = .2, + .tilt = .2, + .orientation = .2, + }, + kTol)); +} + +TEST(StylusStateModelerWithNewProjectionTest, QueryStaleInputsAreDiscarded) { + StylusStateModeler modeler; + modeler.Reset(kDefaultParams); + modeler.Update({1, 1}, Time(0), + {.pressure = .6, .tilt = .5, .orientation = .4}); + modeler.Update({-1, 2}, Time(0.1), + {.pressure = .3, .tilt = .7, .orientation = .6}); + modeler.Update({-4, 0}, Time(0.2), + {.pressure = .9, .tilt = .7, .orientation = .3}); + modeler.Update({-6, -3}, Time(0.3), + {.pressure = .4, .tilt = .3, .orientation = .5}); + modeler.Update({-5, -5}, Time(0.4), + {.pressure = .3, .tilt = .3, .orientation = .1}); + modeler.Update({-3, -4}, Time(0.5), + {.pressure = .6, .tilt = .8, .orientation = .3}); + modeler.Update({-6, -7}, Time(0.6), + {.pressure = .9, .tilt = .8, .orientation = .1}); + modeler.Update({-9, -8}, Time(0.7), + {.pressure = .8, .tilt = .2, .orientation = .2}); + modeler.Update({-11, -5}, Time(0.8), + {.pressure = .2, .tilt = .4, .orientation = .7}); + modeler.Update({-10, -2}, Time(0.9), + {.pressure = .7, .tilt = .3, .orientation = .2}); + + EXPECT_THAT(modeler.Query({2, 0}, {0, 11}, Time(0)), + ResultNear( + { + .position = {1, 1}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = .6, + .tilt = .5, + .orientation = .4, + }, + kTol)); + EXPECT_THAT(modeler.Query({1, 3.5}, {-1, -2.1}, Time(0.1)), + ResultNear({.position = {0.03846, 1.48077}, + .velocity = {-9.61539, 4.80769}, + .acceleration = {-96.15385, 48.07692}, + .time = Time(0.1), + .pressure = 0.455769, + .tilt = 0.596154, + .orientation = 0.496154}, + kTol)); + EXPECT_THAT(modeler.Query({-3, 17. / 6}, {1.1, -1.5}, Time(0.2)), + ResultNear({.position = {-1.93284, 1.37811}, + .velocity = {-23.10945, 0.67164}, + .acceleration = {-168.90547, -24.37812}, + .time = Time(0.2), + .pressure = 0.486567, + .tilt = 0.7, + .orientation = 0.506716}, + kTol)); + + // This causes the point at {1, 1} to be discarded. + modeler.Update({-8, 0}, Time(1), + {.pressure = .6, .tilt = .8, .orientation = .9}); + EXPECT_THAT(modeler.Query({2, 0}, {0, 1.1}, Time(0.1)), + ResultNear( + { + .position = {-1, 2}, + .velocity = {-20, 10}, + .acceleration = {-200, 100}, + .time = Time(0.1), + .pressure = .3, + .tilt = .7, + .orientation = .6, + }, + kTol)); + EXPECT_THAT(modeler.Query({1, 3.5}, {0, 1.1}, Time(0.2)), + ResultNear( + { + .position = {-1, 2}, + .velocity = {-20, 10}, + .acceleration = {-200, 100}, + .time = Time(0.2), + .pressure = .3, + .tilt = .7, + .orientation = .6, + }, + kTol)); + EXPECT_THAT(modeler.Query({-3, 17. / 6}, {1.1, -1.5}, Time(0.3)), + ResultNear({.position = {-1.93284, 1.37811}, + .velocity = {-23.10945, 0.67164}, + .acceleration = {-168.90547, -24.37812}, + .time = Time(0.3), + .pressure = 0.486567, + .tilt = 0.7, + .orientation = 0.506716}, + kTol)); + + // This causes the point at {-1, 2} to be discarded. + modeler.Update({-8, 0}, Time(1.1), + {.pressure = .6, .tilt = .8, .orientation = .9}); + EXPECT_THAT(modeler.Query({2, 0}, {0, 1.1}, Time(0.3)), + ResultNear( + { + .position = {-4, 0}, + .velocity = {-30, -20}, + .acceleration = {-100, -300}, + .time = Time(0.3), + .pressure = .9, + .tilt = .7, + .orientation = .3, + }, + kTol)); + EXPECT_THAT(modeler.Query({1, 3.5}, {0, 1.1}, Time(0.4)), + ResultNear( + { + .position = {-4, 0}, + .velocity = {-30, -20}, + .acceleration = {-100, -300}, + .time = Time(0.4), + .pressure = .9, + .tilt = .7, + .orientation = .3, + }, + kTol)); + EXPECT_THAT(modeler.Query({-3, 17. / 6}, {-6.1, 2}, Time(0.5)), + ResultNear( + { + .position = {-4, 0}, + .velocity = {-30, -20}, + .acceleration = {-100, -300}, + .time = Time(0.5), + .pressure = .9, + .tilt = .7, + .orientation = .3, + }, + kTol)); +} + +TEST(StylusStateModelerWithNewProjectionTest, + QueryCyclicOrientationInterpolation) { + StylusStateModeler modeler; + modeler.Reset(kDefaultParams); + modeler.Update({0, 0}, Time(0), + {.pressure = 0, .tilt = 0, .orientation = 1.8 * kPi}); + modeler.Update({0, 1}, Time(1), + {.pressure = 0, .tilt = 0, .orientation = .2 * kPi}); + modeler.Update({0, 2}, Time(2), + {.pressure = 0, .tilt = 0, .orientation = 1.6 * kPi}); + + EXPECT_NEAR(modeler.Query({0, .25}, {1, 0}, Time(0)).orientation, 1.9 * kPi, + kTol); + EXPECT_NEAR(modeler.Query({0, .75}, {1, 0}, Time(1)).orientation, .1 * kPi, + kTol); + EXPECT_NEAR(modeler.Query({0, 1.25}, {1, 0}, Time(1.5)).orientation, + .05 * kPi, kTol); + EXPECT_NEAR(modeler.Query({0, 1.75}, {1, 0}, Time(2)).orientation, 1.75 * kPi, + kTol); +} + +TEST(StylusStateModelerWithNewProjectionTest, QueryAndReset) { + StylusStateModeler modeler; + modeler.Reset(kDefaultParams); + + modeler.Update({4, 5}, Time(0), + {.pressure = .4, .tilt = .9, .orientation = .1}); + modeler.Update({7, 8}, Time(1), + {.pressure = .1, .tilt = .2, .orientation = .5}); + EXPECT_THAT(modeler.Query({10, 12}, {0.51, -0.5}, Time(0)), + ResultNear( + { + .position = {7, 8}, + .velocity = {3, 3}, + .acceleration = {3, 3}, + .time = Time(0), + .pressure = .1, + .tilt = .2, + .orientation = .5, + }, + kTol)); + + modeler.Reset(kDefaultParams); + EXPECT_EQ(modeler.Query({10, 12}, {0.5, -0.51}, Time(0)), kUnknownResult); + + modeler.Update({-1, 4}, Time(2), + {.pressure = .4, .tilt = .6, .orientation = .8}); + EXPECT_THAT(modeler.Query({6, 7}, {-7, -3.1}, Time(2)), + ResultNear( + { + .position = {-1, 4}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(2), + .pressure = .4, + .tilt = .6, + .orientation = .8, + }, + kTol)); + + modeler.Update({-3, 0}, Time(3), + {.pressure = .7, .tilt = .2, .orientation = .5}); + EXPECT_THAT(modeler.Query({-2, 2}, {0, 1.1}, Time(2.5)), + ResultNear( + { + .position = {-2, 2}, + .velocity = {-1, -2}, + .acceleration = {-1, -2}, + .time = Time(2.5), + .pressure = .55, + .tilt = .4, + .orientation = .65, + }, + kTol)); + EXPECT_THAT(modeler.Query({0, 5}, {-0.41, 0.2}, Time(0)), + ResultNear( + { + .position = {-1, 4}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = .4, + .tilt = .6, + .orientation = .8, + }, + kTol)); +} + +TEST(StylusStateModelerWithNewProjectionTest, ModelPressureOnly) { + StylusStateModeler modeler; + modeler.Reset(kDefaultParams); + + modeler.Update({0, 0}, Time(0), + {.pressure = .5, .tilt = -2, .orientation = -.1}); + EXPECT_THAT(modeler.Query({1, 1}, {0, 1.1}, Time(0)), + ResultNear( + { + .position = {0, 0}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = .5, + .tilt = -1, + .orientation = -1, + }, + kTol)); + + modeler.Update({2, 0}, Time(1), + {.pressure = .7, .tilt = -2, .orientation = -.1}); + EXPECT_THAT(modeler.Query({1, 1}, {0, -1.1}, Time(1)), + ResultNear( + { + .position = {1, 0}, + .velocity = {1, 0}, + .acceleration = {1, 0}, + .time = Time(1), + .pressure = .6, + .tilt = -1, + .orientation = -1, + }, + kTol)); +} + +TEST(StylusStateModelerWithNewProjectionTest, ModelTiltOnly) { + StylusStateModeler modeler; + modeler.Reset(kDefaultParams); + + modeler.Update({0, 0}, Time(0), + {.pressure = -2, .tilt = .5, .orientation = -.1}); + EXPECT_THAT(modeler.Query({1, 1}, {0, 1.1}, Time(0)), + ResultNear( + { + .position = {0, 0}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = -1, + .tilt = .5, + .orientation = -1, + }, + kTol)); + + modeler.Update({2, 0}, Time(1), + {.pressure = -2, .tilt = .3, .orientation = -.1}); + EXPECT_THAT(modeler.Query({1, 1}, {0, -1.1}, Time(1)), + ResultNear( + { + .position = {1, 0}, + .velocity = {1, 0}, + .acceleration = {1, 0}, + .time = Time(1), + .pressure = -1, + .tilt = .4, + .orientation = -1, + }, + kTol)); +} + +TEST(StylusStateModelerWithNewProjectionTest, ModelOrientationOnly) { + StylusStateModeler modeler; + modeler.Reset(kDefaultParams); + + modeler.Update({0, 0}, Time(0), + {.pressure = -2, .tilt = -.1, .orientation = 1}); + EXPECT_THAT(modeler.Query({1, 1}, {0, 1.1}, Time(0)), + ResultNear( + { + .position = {0, 0}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = -1, + .tilt = -1, + .orientation = 1, + }, + kTol)); + + modeler.Update({2, 0}, Time(1), + {.pressure = -2, .tilt = -.3, .orientation = 2}); + EXPECT_THAT(modeler.Query({1, 1}, {0, -1.1}, Time(1)), + ResultNear( + { + .position = {1, 0}, + .velocity = {1, 0}, + .acceleration = {1, 0}, + .time = Time(1), + .pressure = -1, + .tilt = -1, + .orientation = 1.5, + }, + kTol)); +} + +TEST(StylusStateModelerWithNewProjectionTest, DropFieldsOneByOne) { + StylusStateModeler modeler; + modeler.Reset(kDefaultParams); + + modeler.Update({0, 0}, Time(0), + {.pressure = .5, .tilt = .5, .orientation = .5}); + EXPECT_THAT(modeler.Query({1, 0}, {0, 1.1}, Time(0)), + ResultNear( + { + .position = {0, 0}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = .5, + .tilt = .5, + .orientation = .5, + }, + kTol)); + + modeler.Update({2, 0}, Time(1), + {.pressure = .3, .tilt = .7, .orientation = -1}); + EXPECT_THAT(modeler.Query({1, 0}, {0, 1.1}, Time(1)), + ResultNear( + { + .position = {1, 0}, + .velocity = {1, 0}, + .acceleration = {1, 0}, + .time = Time(1), + .pressure = .4, + .tilt = .6, + .orientation = -1, + }, + kTol)); + + modeler.Update({4, 0}, Time(2), + {.pressure = .1, .tilt = -1, .orientation = 1}); + EXPECT_THAT(modeler.Query({3, 0}, {0, 1.1}, Time(2)), + ResultNear( + { + .position = {3, 0}, + .velocity = {2, 0}, + .acceleration = {1, 0}, + .time = Time(2), + .pressure = .2, + .tilt = -1, + .orientation = -1, + }, + kTol)); + + modeler.Update({6, 0}, Time(3), + {.pressure = -1, .tilt = .2, .orientation = 0}); + EXPECT_THAT(modeler.Query({5, 0}, {0, 1.1}, Time(3)), + ResultNear( + { + .position = {5, 0}, + .velocity = {2, 0}, + .acceleration = {0, 0}, + .time = Time(3), + .pressure = -1, + .tilt = -1, + .orientation = -1, + }, + kTol)); + + modeler.Update({8, 0}, Time(4), + {.pressure = .3, .tilt = .4, .orientation = .5}); + EXPECT_THAT(modeler.Query({7, 0}, {0, 1.1}, Time(4)), + ResultNear( + { + .position = {7, 0}, + .velocity = {2, 0}, + .acceleration = {0, 0}, + .time = Time(4), + .pressure = -1, + .tilt = -1, + .orientation = -1, + }, + kTol)); + + modeler.Reset(kDefaultParams); + EXPECT_THAT(modeler.Query({1, 0}, {0, 1.1}, Time(0)), + ResultNear(kUnknownResult, kTol)); + + modeler.Update({0, 0}, Time(5), + {.pressure = .1, .tilt = .8, .orientation = .3}); + EXPECT_THAT(modeler.Query({1, 0}, {0, 1.1}, Time(5)), + ResultNear( + { + .position = {0, 0}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(5), + .pressure = .1, + .tilt = .8, + .orientation = .3, + }, + kTol)); +} + +TEST(StylusStateModelerWithNewProjectionTest, SaveAndRestore) { + StylusStateModeler modeler; + modeler.Reset(kDefaultParams); + modeler.Update({1, 1}, Time(0), + {.pressure = .6, .tilt = .5, .orientation = .4}); + modeler.Update({-1, 2}, Time(1), + {.pressure = .3, .tilt = .7, .orientation = .6}); + modeler.Update({-4, 0}, Time(2), + {.pressure = .9, .tilt = .7, .orientation = .3}); + modeler.Update({-6, -3}, Time(3), + {.pressure = .4, .tilt = .3, .orientation = .5}); + modeler.Update({-5, -5}, Time(4), + {.pressure = .3, .tilt = .3, .orientation = .1}); + modeler.Update({-3, -4}, Time(5), + {.pressure = .6, .tilt = .8, .orientation = .3}); + modeler.Update({-6, -7}, Time(6), + {.pressure = .9, .tilt = .8, .orientation = .1}); + modeler.Update({-9, -8}, Time(7), + {.pressure = .8, .tilt = .2, .orientation = .2}); + modeler.Update({-11, -5}, Time(8), + {.pressure = .2, .tilt = .4, .orientation = .7}); + modeler.Update({-10, -2}, Time(9), + {.pressure = .7, .tilt = .3, .orientation = .2}); + + ASSERT_THAT(modeler.Query({2, 0}, {0, 1.1}, Time(0)), + ResultNear( + { + .position = {1, 1}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = .6, + .tilt = .5, + .orientation = .4, + }, + kTol)); + + // Calling restore with no save should have no effect. + modeler.Restore(); + + EXPECT_THAT(modeler.Query({2, 0}, {0, 1.1}, Time(0)), + ResultNear( + { + .position = {1, 1}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = .6, + .tilt = .5, + .orientation = .4, + }, + kTol)); + + modeler.Save(); + + // This causes the points at {1, 1} and {-1, 2} to be discarded. + modeler.Update({-8, 0}, Time(10), + {.pressure = .6, .tilt = .8, .orientation = .9}); + modeler.Update({-8, 0}, Time(11), + {.pressure = .6, .tilt = .8, .orientation = .9}); + EXPECT_THAT(modeler.Query({2, 0}, {0, 1.1}, Time(0)), + ResultNear( + { + .position = {-4, 0}, + .velocity = {-3, -2}, + .acceleration = {-1, -3}, + .time = Time(0), + .pressure = .9, + .tilt = .7, + .orientation = .3, + }, + kTol)); + + // Restoring should revert the updates. + modeler.Restore(); + EXPECT_THAT(modeler.Query({2, 0}, {0, 1.1}, Time(0)), + ResultNear( + { + .position = {1, 1}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = .6, + .tilt = .5, + .orientation = .4, + }, + kTol)); + + // Restoring should not have cleared the saved state, so we can repeat the + // action. + modeler.Update({-8, 0}, Time(12), + {.pressure = .6, .tilt = .8, .orientation = .9}); + modeler.Update({-8, 0}, Time(13), + {.pressure = .6, .tilt = .8, .orientation = .9}); + EXPECT_THAT(modeler.Query({2, 0}, {0, 1.1}, Time(0)), + ResultNear( + { + .position = {-4, 0}, + .velocity = {-3, -2}, + .acceleration = {-1, -3}, + .time = Time(0), + .pressure = .9, + .tilt = .7, + .orientation = .3, + }, + kTol)); + modeler.Restore(); + EXPECT_THAT(modeler.Query({2, 0}, {0, 1.1}, Time(0)), + ResultNear( + { + .position = {1, 1}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = .6, + .tilt = .5, + .orientation = .4, + }, + kTol)); + + // Calling Reset should clear the save point so that calling Restore should + // have no effect. + modeler.Reset(kDefaultParams); + modeler.Update({-1, 4}, Time(14), + {.pressure = .4, .tilt = .6, .orientation = .8}); + EXPECT_THAT(modeler.Query({6, 7}, {0, 1.1}, Time(0)), + ResultNear( + { + .position = {-1, 4}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = .4, + .tilt = .6, + .orientation = .8, + }, + kTol)); + modeler.Restore(); + EXPECT_THAT(modeler.Query({6, 7}, {0, 1.1}, Time(0)), + ResultNear( + { + .position = {-1, 4}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = .4, + .tilt = .6, + .orientation = .8, + }, + kTol)); +} + +} // namespace +} // namespace stroke_model +} // namespace ink diff --git a/ink_stroke_modeler/params.cc b/ink_stroke_modeler/params.cc index ee3b2df..322441e 100644 --- a/ink_stroke_modeler/params.cc +++ b/ink_stroke_modeler/params.cc @@ -40,8 +40,57 @@ constexpr int kMaxEndOfStrokeMaxIterations = 1000; } // namespace +absl::Status ValidateLoopContractionMitigationParameters( + const PositionModelerParams::LoopContractionMitigationParameters& params, + bool project_to_segment_along_normal_is_enabled) { + if (!params.is_enabled) return absl::OkStatus(); + if (params.is_enabled && !project_to_segment_along_normal_is_enabled) { + return absl::InvalidArgumentError( + "`StylusStateModelerParams::project_to_segment_along_normal_is_enabled`" + " must be true if " + "`PositionModelerParams::LoopContractionMitigationParameters::is_" + "enabled` is true."); + } + if (params.speed_lower_bound > params.speed_upper_bound || + params.speed_lower_bound < 0) { + return absl::InvalidArgumentError(absl::Substitute( + "LoopContractionMitigationParameters::speed_lower_bound must be " + "smaller or equal to " + "LoopContractionMitigationParameters::speed_upper_bound and greater " + "than or equal to 0. Actual " + "values: lower_bound:$0, upper_bound: $1", + params.speed_lower_bound, params.speed_upper_bound)); + } + + if (params.interpolation_strength_at_speed_lower_bound < + params.interpolation_strength_at_speed_upper_bound || + params.interpolation_strength_at_speed_lower_bound > 1 || + params.interpolation_strength_at_speed_upper_bound < 0) { + return absl::InvalidArgumentError(absl::Substitute( + "LoopContractionMitigationParameters::interpolation_strength_at_speed_" + "lower_bound must be greater or equal to " + "LoopContractionMitigationParameters::interpolation_strength_at_speed_" + "upper_bound and in the interval [0, 1]. Actual values: " + "at_lower_bound:$0, at_upper_bound: $1", + params.interpolation_strength_at_speed_lower_bound, + params.interpolation_strength_at_speed_upper_bound)); + } + + if (params.n_speed_samples < 1 || params.n_speed_samples > 10000) { + return absl::InvalidArgumentError(absl::Substitute( + "LoopContractionMitigationParameters::n_speed_samples must be in the " + "interval [1, 10000]. Actual value: $0", + params.n_speed_samples)); + } + return absl::OkStatus(); +} + absl::Status ValidatePositionModelerParams( - const PositionModelerParams& params) { + const PositionModelerParams& params, + bool project_to_segment_along_normal_is_enabled) { + RETURN_IF_ERROR(ValidateLoopContractionMitigationParameters( + params.loop_contraction_mitigation_params, + project_to_segment_along_normal_is_enabled)); RETURN_IF_ERROR( ValidateGreaterThanZero(params.spring_mass_constant, "PositionModelerParams::spring_mass_constant")); @@ -182,8 +231,10 @@ absl::Status ValidatePredictionParams(const PredictionParams& params) { absl::Status ValidateStrokeModelParams(const StrokeModelParams& params) { RETURN_IF_ERROR(ValidateWobbleSmootherParams(params.wobble_smoother_params)); - RETURN_IF_ERROR( - ValidatePositionModelerParams(params.position_modeler_params)); + RETURN_IF_ERROR(ValidatePositionModelerParams( + params.position_modeler_params, + params.stylus_state_modeler_params + .project_to_segment_along_normal_is_enabled)); RETURN_IF_ERROR(ValidateSamplingParams(params.sampling_params)); RETURN_IF_ERROR( ValidateStylusStateModelerParams(params.stylus_state_modeler_params)); diff --git a/ink_stroke_modeler/params.h b/ink_stroke_modeler/params.h index d5006ba..a96549c 100644 --- a/ink_stroke_modeler/params.h +++ b/ink_stroke_modeler/params.h @@ -17,7 +17,6 @@ #ifndef INK_STROKE_MODELER_PARAMS_H_ #define INK_STROKE_MODELER_PARAMS_H_ -#include #include #include "absl/status/status.h" @@ -53,6 +52,50 @@ struct PositionModelerParams { // The ratio of the pen's velocity that is subtracted from the pen's // acceleration per unit time, to simulate drag. float drag_constant = 72.f; + + // These parameters control the behavior of the loop contraction mitigation. + // The mitigation corrects for loop contraction by interpolating between the + // result from the spring model, and the nearest point on the raw input + // polyline, based on the a moving average of the speed of the raw inputs. + struct LoopContractionMitigationParameters { + // To keep legacy behavior consistent, default to not using the mitigation. + bool is_enabled = false; + + // The slowest speed at which to start applying the mitigation. At this + // speed, the interpolation value will be equal to + // `interpolation_strength_at_speed_lower_bound`. + // When is_enabled is true, this must be <= `speed_upper_bound` and >= 0. + float speed_lower_bound = -1; + // The fastest speed at which to start applying the mitigation. At this + // speed, the interpolation value will be equal to + // `interpolation_strength_at_speed_upper_bound`. + // When is_enabled is true, this must be >= `speed_lower_bound` and >= 0. + float speed_upper_bound = -1; + + // The interpolation value to use when the speed is equal to + // `speed_lower_bound`. A value of 1 results in no mitigation, using the + // unaltered result of the spring model. A value of 0 uses the value from + // the raw input polyline, with no influence from the spring model. + // When is_enabled is true, this must be >= + // `interpolation_strength_at_speed_upper_bound` and <= 1. + float interpolation_strength_at_speed_lower_bound = -1; + // The interpolation value to use when the speed is equal to + // `speed_upper_bound`. A value of 1 results in no mitigation, using the + // unaltered result of the spring model. A value of 0 uses the value from + // the raw input polyline, with no influence from the spring model. + // When is_enabled is true, this must be <= + // `interpolation_strength_at_speed_lower_bound` and >= 0. + float interpolation_strength_at_speed_upper_bound = -1; + + // The number of samples to use when calculating the moving average of the + // speed. A higher number results in a smoother transition between low and + // high speeds (which can reduce artifacts from noisy inputs) at the cost of + // adding latency to the mitigation response. + // When is_enabled is true, this must be >= 1 and <= 10k. + int n_speed_samples = -1; + }; + + LoopContractionMitigationParameters loop_contraction_mitigation_params; }; // These parameters are used for sampling. @@ -94,6 +137,12 @@ struct StylusStateModelerParams { // The maximum number of raw inputs to look at when finding the nearest states // for interpolation. int max_input_samples = 10; + + // When this is set to false, a call to `Query` will use + // `NearestPointOnSegment` to determine the distance of the position to the + // segment. When this is set to true, it will `ProjectToSegmentAlongNormal` + // instead. + bool project_to_segment_along_normal_is_enabled = false; }; // These parameters are used for applying smoothing to the input to reduce @@ -240,7 +289,9 @@ struct StrokeModelParams { // These validation functions will return an error if the given parameters are // invalid. -absl::Status ValidatePositionModelerParams(const PositionModelerParams& params); +absl::Status ValidatePositionModelerParams( + const PositionModelerParams& params, + bool project_to_segment_along_normal_is_enabled); absl::Status ValidateSamplingParams(const SamplingParams& params); absl::Status ValidateStylusStateModelerParams( const StylusStateModelerParams& params); diff --git a/ink_stroke_modeler/params_test.cc b/ink_stroke_modeler/params_test.cc index 3036bdc..18606fc 100644 --- a/ink_stroke_modeler/params_test.cc +++ b/ink_stroke_modeler/params_test.cc @@ -45,24 +45,73 @@ const KalmanPredictorParams kGoodKalmanParams{ const StrokeModelParams kGoodStrokeModelParams{ .wobble_smoother_params{ .timeout = Duration(.5), .speed_floor = 1, .speed_ceiling = 20}, - .position_modeler_params{.spring_mass_constant = .2, .drag_constant = 4}, + .position_modeler_params{ + .spring_mass_constant = .2, + .drag_constant = 4, + .loop_contraction_mitigation_params = + {.is_enabled = true, + .speed_lower_bound = 0, + .speed_upper_bound = 60, + .interpolation_strength_at_speed_lower_bound = 1, + .interpolation_strength_at_speed_upper_bound = 0, + .n_speed_samples = 10}}, .sampling_params{.min_output_rate = 3, .end_of_stroke_stopping_distance = 1e-6, .end_of_stroke_max_iterations = 1}, - .stylus_state_modeler_params{.max_input_samples = 7}, + .stylus_state_modeler_params{ + .max_input_samples = 7, + .project_to_segment_along_normal_is_enabled = true}, .prediction_params = StrokeEndPredictorParams{}}; TEST(ParamsTest, ValidatePositionModelerParams) { EXPECT_TRUE(ValidatePositionModelerParams( - {.spring_mass_constant = 1, .drag_constant = 3}) + {.spring_mass_constant = 1, + .drag_constant = 3, + .loop_contraction_mitigation_params = + {.is_enabled = true, + .speed_lower_bound = 0, + .speed_upper_bound = 60, + .interpolation_strength_at_speed_lower_bound = 1, + .interpolation_strength_at_speed_upper_bound = 0, + + .n_speed_samples = 10}}, + /*project_to_segment_along_normal_is_enabled=*/true) .ok()); EXPECT_EQ(ValidatePositionModelerParams( - {.spring_mass_constant = 0, .drag_constant = 1}) + {.spring_mass_constant = 0, .drag_constant = 1}, + /*project_to_segment_along_normal_is_enabled=*/false) + .code(), + absl::StatusCode::kInvalidArgument); + EXPECT_EQ(ValidatePositionModelerParams( + {.spring_mass_constant = 1, .drag_constant = 0}, + /*project_to_segment_along_normal_is_enabled=*/false) + .code(), + absl::StatusCode::kInvalidArgument); + EXPECT_EQ(ValidatePositionModelerParams( + {.spring_mass_constant = 1, + .drag_constant = 3, + .loop_contraction_mitigation_params = + {.is_enabled = true, + .speed_lower_bound = 0, + .speed_upper_bound = 5, + .interpolation_strength_at_speed_lower_bound = 1, + .interpolation_strength_at_speed_upper_bound = 0, + .n_speed_samples = -1}}, + /*project_to_segment_along_normal_is_enabled=*/true) .code(), absl::StatusCode::kInvalidArgument); EXPECT_EQ(ValidatePositionModelerParams( - {.spring_mass_constant = 1, .drag_constant = 0}) + {.spring_mass_constant = 1, + .drag_constant = 3, + .loop_contraction_mitigation_params = + {.is_enabled = true, + .speed_lower_bound = 0, + .speed_upper_bound = 5, + .interpolation_strength_at_speed_lower_bound = 1, + .interpolation_strength_at_speed_upper_bound = 0, + .n_speed_samples = 5}}, + /*project_to_segment_along_normal_is_enabled=*/false) .code(), absl::StatusCode::kInvalidArgument); } diff --git a/ink_stroke_modeler/stroke_modeler.cc b/ink_stroke_modeler/stroke_modeler.cc index 6458077..db7324c 100644 --- a/ink_stroke_modeler/stroke_modeler.cc +++ b/ink_stroke_modeler/stroke_modeler.cc @@ -24,11 +24,13 @@ #include "absl/status/status.h" #include "absl/status/statusor.h" #include "ink_stroke_modeler/internal/internal_types.h" +#include "ink_stroke_modeler/internal/loop_contraction_mitigation_modeler.h" #include "ink_stroke_modeler/internal/position_modeler.h" #include "ink_stroke_modeler/internal/prediction/input_predictor.h" #include "ink_stroke_modeler/internal/prediction/kalman_predictor.h" #include "ink_stroke_modeler/internal/prediction/stroke_end_predictor.h" #include "ink_stroke_modeler/internal/stylus_state_modeler.h" +#include "ink_stroke_modeler/internal/utils.h" #include "ink_stroke_modeler/params.h" #include "ink_stroke_modeler/types.h" @@ -36,19 +38,62 @@ namespace ink { namespace stroke_model { namespace { -void ModelStylus(const std::vector &tip_states, - const StylusStateModeler &stylus_state_modeler, - std::vector &result) { +Result InterpResult(const Result &from, const Result &to, float interp_value) { + return { + .position = Interp(from.position, to.position, interp_value), + .velocity = Interp(from.velocity, to.velocity, interp_value), + .acceleration = Interp(from.acceleration, to.acceleration, interp_value), + .time = Interp(from.time, to.time, interp_value), + .pressure = Interp(from.pressure, to.pressure, interp_value), + .tilt = Interp(from.tilt, to.tilt, interp_value), + .orientation = Interp(from.orientation, to.orientation, interp_value), + }; +} + +Vec2 GetStrokeNormal(const TipState &tip_state, Time prev_time) { + auto unit = [](Vec2 x) { return x / x.Magnitude(); }; + + Vec2 stroke_dir = + unit(tip_state.velocity) + + unit(tip_state.velocity + + tip_state.acceleration * (tip_state.time - prev_time).Value()); + return stroke_dir.Magnitude() < 0.0001 * tip_state.velocity.Magnitude() + ? Vec2{-tip_state.velocity.y, tip_state.velocity.x} + : Vec2{-stroke_dir.y, stroke_dir.x}; +} + +Result MakeResultFromTipState(const TipState &tipstate, + const Result &stylus_state) { + return { + .position = tipstate.position, + .velocity = tipstate.velocity, + .acceleration = tipstate.acceleration, + .time = tipstate.time, + .pressure = stylus_state.pressure, + .tilt = stylus_state.tilt, + .orientation = stylus_state.orientation, + }; +} + +void ModelStylus( + const std::vector &tip_states, + const StylusStateModeler &stylus_state_modeler, + LoopContractionMitigationModeler &loop_contraction_mitigation_modeler, + std::vector &result, Time prev_time) { result.reserve(tip_states.size()); + + float interp_value = + loop_contraction_mitigation_modeler.GetInterpolationValue(); for (const auto &tip_state : tip_states) { - auto stylus_state = stylus_state_modeler.Query(tip_state.position); - result.push_back({.position = tip_state.position, - .velocity = tip_state.velocity, - .acceleration = tip_state.acceleration, - .time = tip_state.time, - .pressure = stylus_state.pressure, - .tilt = stylus_state.tilt, - .orientation = stylus_state.orientation}); + Vec2 stroke_normal = GetStrokeNormal(tip_state, prev_time); + Result projected_state = stylus_state_modeler.Query( + tip_state.position, stroke_normal, tip_state.time); + Result modeled_state = MakeResultFromTipState(tip_state, projected_state); + result.push_back( + InterpResult(projected_state, modeled_state, interp_value)); + interp_value = + loop_contraction_mitigation_modeler.Update(result.back().velocity); + prev_time = tip_state.time; } } @@ -83,6 +128,9 @@ absl::Status StrokeModeler::Reset( prediction_params)) { predictor_ = nullptr; } + loop_contraction_mitigation_modeler_.Reset( + stroke_model_params_->position_modeler_params + .loop_contraction_mitigation_params); return absl::OkStatus(); } @@ -152,7 +200,10 @@ absl::Status StrokeModeler::Predict(std::vector &results) const { predictor_->ConstructPrediction(position_modeler_.CurrentState(), tip_state_buffer_); - ModelStylus(tip_state_buffer_, stylus_state_modeler_, results); + LoopContractionMitigationModeler prediction_loop_modeler = + loop_contraction_mitigation_modeler_; + ModelStylus(tip_state_buffer_, stylus_state_modeler_, prediction_loop_modeler, + results, last_input_->input.time); return absl::OkStatus(); } @@ -172,7 +223,11 @@ absl::Status StrokeModeler::ProcessDownEvent(const Input &input, stroke_model_params_->position_modeler_params); stylus_state_modeler_.Reset( stroke_model_params_->stylus_state_modeler_params); - stylus_state_modeler_.Update(input.position, + loop_contraction_mitigation_modeler_.Reset( + stroke_model_params_->position_modeler_params + .loop_contraction_mitigation_params); + + stylus_state_modeler_.Update(input.position, input.time, {.pressure = input.pressure, .tilt = input.tilt, .orientation = input.orientation}); @@ -231,15 +286,17 @@ absl::Status StrokeModeler::ProcessUpEvent(const Input &input, tip_state_buffer_.push_back(position_modeler_.CurrentState()); } - stylus_state_modeler_.Update(input.position, + stylus_state_modeler_.Update(input.position, input.time, {.pressure = input.pressure, .tilt = input.tilt, .orientation = input.orientation}); + ModelStylus(tip_state_buffer_, stylus_state_modeler_, + loop_contraction_mitigation_modeler_, results, + last_input_->input.time); // This indicates that we've finished the stroke. last_input_ = std::nullopt; - ModelStylus(tip_state_buffer_, stylus_state_modeler_, results); return absl::OkStatus(); } @@ -251,10 +308,12 @@ absl::Status StrokeModeler::ProcessMoveEvent(const Input &input, } Vec2 corrected_position = wobble_smoother_.Update(input.position, input.time); - stylus_state_modeler_.Update(corrected_position, - {.pressure = input.pressure, - .tilt = input.tilt, - .orientation = input.orientation}); + stylus_state_modeler_.Update(corrected_position, input.time, + { + .pressure = input.pressure, + .tilt = input.tilt, + .orientation = input.orientation, + }); absl::StatusOr n_steps = NumberOfStepsBetweenInputs( position_modeler_.CurrentState(), last_input_->input, input, @@ -274,7 +333,9 @@ absl::Status StrokeModeler::ProcessMoveEvent(const Input &input, predictor_->Update(corrected_position, input.time); } last_input_ = {.input = input, .corrected_position = corrected_position}; - ModelStylus(tip_state_buffer_, stylus_state_modeler_, results); + ModelStylus(tip_state_buffer_, stylus_state_modeler_, + loop_contraction_mitigation_modeler_, results, + last_input_->input.time); return absl::OkStatus(); } @@ -282,6 +343,7 @@ void StrokeModeler::Save() { wobble_smoother_.Save(); position_modeler_.Save(); stylus_state_modeler_.Save(); + loop_contraction_mitigation_modeler_.Save(); saved_last_input_ = last_input_; if (predictor_ != nullptr) { saved_predictor_ = predictor_->MakeCopy(); @@ -295,6 +357,7 @@ void StrokeModeler::Restore() { wobble_smoother_.Restore(); position_modeler_.Restore(); stylus_state_modeler_.Restore(); + loop_contraction_mitigation_modeler_.Restore(); last_input_ = saved_last_input_; if (saved_predictor_ != nullptr) { predictor_ = saved_predictor_->MakeCopy(); diff --git a/ink_stroke_modeler/stroke_modeler.h b/ink_stroke_modeler/stroke_modeler.h index 84624b0..f3da696 100644 --- a/ink_stroke_modeler/stroke_modeler.h +++ b/ink_stroke_modeler/stroke_modeler.h @@ -23,6 +23,7 @@ #include "absl/status/status.h" #include "ink_stroke_modeler/internal/internal_types.h" +#include "ink_stroke_modeler/internal/loop_contraction_mitigation_modeler.h" #include "ink_stroke_modeler/internal/position_modeler.h" #include "ink_stroke_modeler/internal/prediction/input_predictor.h" #include "ink_stroke_modeler/internal/stylus_state_modeler.h" @@ -120,6 +121,7 @@ class StrokeModeler { WobbleSmoother wobble_smoother_; PositionModeler position_modeler_; StylusStateModeler stylus_state_modeler_; + LoopContractionMitigationModeler loop_contraction_mitigation_modeler_; // This buffer is used as optimization to avoid re-allocating the vector in // the predictor but doesn't hold state between calls, so can be mutable. diff --git a/ink_stroke_modeler/stroke_modeler_test.cc b/ink_stroke_modeler/stroke_modeler_test.cc index cb7c091..203da62 100644 --- a/ink_stroke_modeler/stroke_modeler_test.cc +++ b/ink_stroke_modeler/stroke_modeler_test.cc @@ -14,28 +14,23 @@ #include "ink_stroke_modeler/stroke_modeler.h" +#include #include #include "gmock/gmock.h" #include "gtest/gtest.h" #include "absl/status/status.h" -#include "absl/strings/str_cat.h" -#include "ink_stroke_modeler/internal/type_matchers.h" #include "ink_stroke_modeler/params.h" +#include "ink_stroke_modeler/type_matchers.h" #include "ink_stroke_modeler/types.h" namespace ink { namespace stroke_model { namespace { -using ::testing::AllOf; using ::testing::ElementsAre; -using ::testing::ExplainMatchResult; -using ::testing::Field; -using ::testing::FloatNear; using ::testing::IsEmpty; using ::testing::Not; -using ::testing::PrintToString; constexpr float kTol = 1e-4; @@ -43,39 +38,24 @@ constexpr float kTol = 1e-4; const StrokeModelParams kDefaultParams{ .wobble_smoother_params{ .timeout = Duration(.04), .speed_floor = 1.31, .speed_ceiling = 1.44}, - .position_modeler_params{.spring_mass_constant = 11.f / 32400, - .drag_constant = 72.f}, + .position_modeler_params{ + .spring_mass_constant = 11.f / 32400, + .drag_constant = 72.f, + .loop_contraction_mitigation_params = + {.is_enabled = false, + .speed_lower_bound = -1, + .speed_upper_bound = -1, + .interpolation_strength_at_speed_lower_bound = -1, + .interpolation_strength_at_speed_upper_bound = -1, + .n_speed_samples = -1}}, .sampling_params{.min_output_rate = 180, .end_of_stroke_stopping_distance = .001, .end_of_stroke_max_iterations = 20}, - .stylus_state_modeler_params{.max_input_samples = 20}, + .stylus_state_modeler_params{ + .max_input_samples = 20, + .project_to_segment_along_normal_is_enabled = false}, .prediction_params = StrokeEndPredictorParams()}; -MATCHER_P2(ResultNearMatcher, expected, tolerance, - absl::StrCat(negation ? "doesn't approximately match" - : "approximately matches", - " Result (expected: ", PrintToString(expected), - ", tolerance: ", PrintToString(tolerance), ")")) { - return ExplainMatchResult( - AllOf(Field("position", &Result::position, - Vec2Near(expected.position, tolerance)), - Field("velocity", &Result::velocity, - Vec2Near(expected.velocity, tolerance)), - Field("acceleration", &Result::acceleration, - Vec2Near(expected.acceleration, tolerance)), - Field("time", &Result::time, TimeNear(expected.time, tolerance)), - Field("pressure", &Result::pressure, - FloatNear(expected.pressure, tolerance)), - Field("tilt", &Result::tilt, FloatNear(expected.tilt, tolerance)), - Field("orientation", &Result::orientation, - FloatNear(expected.orientation, tolerance))), - arg, result_listener); -} - -::testing::Matcher ResultNear(const Result &expected, float tolerance) { - return ResultNearMatcher(expected, tolerance); -} - TEST(StrokeModelerTest, NoPredictionUponInit) { StrokeModeler modeler; ASSERT_TRUE(modeler.Reset(kDefaultParams).ok()); @@ -2253,6 +2233,55 @@ TEST(StrokeModelerTest, SaveAndRestore) { absl::StatusCode::kFailedPrecondition); } +TEST(StrokeModelerTest, UpdateToUpdateIdenticalToUpdatePredictUpdate) { + Input input1 = {.event_type = Input::EventType::kDown, + .position = {-6, -2}, + .time = Time(0), + .pressure = 0.5, + .tilt = 0.5, + .orientation = 0.5}; + + Input input2 = {.event_type = Input::EventType::kMove, + .position = {-6.2, -2}, + .time = Time(.0167), + .pressure = 0.48, + .tilt = 0.4, + .orientation = 0.4}; + Input input3 = {.event_type = Input::EventType::kMove, + .position = {-6.2, -2.2}, + .time = Time(0.0334), + .pressure = 0.48, + .tilt = 0.4, + .orientation = 0.4}; + + StrokeModeler modeler; + ASSERT_TRUE(modeler.Reset(kDefaultParams).ok()); + + std::vector results_with_predict; + std::vector results_without_predict; + + // Run the first 2 input updates, then a prediction then the last input. + ASSERT_TRUE(modeler.Update(input1, results_with_predict).ok()); + results_with_predict.clear(); + ASSERT_TRUE(modeler.Update(input2, results_with_predict).ok()); + results_with_predict.clear(); + ASSERT_TRUE(modeler.Predict(results_with_predict).ok()); + results_with_predict.clear(); + ASSERT_TRUE(modeler.Update(input3, results_with_predict).ok()); + + // Reset the modeler and run the same 3 input updates again but without + // prediction. + ASSERT_TRUE(modeler.Reset(kDefaultParams).ok()); + ASSERT_TRUE(modeler.Update(input1, results_without_predict).ok()); + results_without_predict.clear(); + ASSERT_TRUE(modeler.Update(input2, results_without_predict).ok()); + results_without_predict.clear(); + ASSERT_TRUE(modeler.Update(input3, results_without_predict).ok()); + + // The results for both runs should be the same. + EXPECT_EQ(results_with_predict, results_without_predict); +} + } // namespace } // namespace stroke_model } // namespace ink diff --git a/ink_stroke_modeler/stroke_modeler_with_new_projection_test.cc b/ink_stroke_modeler/stroke_modeler_with_new_projection_test.cc new file mode 100644 index 0000000..85c69bd --- /dev/null +++ b/ink_stroke_modeler/stroke_modeler_with_new_projection_test.cc @@ -0,0 +1,2591 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "absl/status/status.h" +#include "ink_stroke_modeler/params.h" +#include "ink_stroke_modeler/stroke_modeler.h" +#include "ink_stroke_modeler/type_matchers.h" +#include "ink_stroke_modeler/types.h" + +namespace ink { +namespace stroke_model { +namespace { + +using ::testing::ElementsAre; +using ::testing::IsEmpty; +using ::testing::Not; + +constexpr float kTol = 1e-4; + +// These parameters use cm for distance and seconds for time. +const StrokeModelParams kDefaultParams{ + .wobble_smoother_params{ + .timeout = Duration(.04), .speed_floor = 1.31, .speed_ceiling = 1.44}, + .position_modeler_params{ + .spring_mass_constant = 11.f / 32400, + .drag_constant = 72.f, + .loop_contraction_mitigation_params = + {.is_enabled = false, + .speed_lower_bound = -1, + .speed_upper_bound = -1, + .interpolation_strength_at_speed_lower_bound = -1, + .interpolation_strength_at_speed_upper_bound = -1, + .n_speed_samples = -1}}, + .sampling_params{.min_output_rate = 180, + .end_of_stroke_stopping_distance = .001, + .end_of_stroke_max_iterations = 20}, + .stylus_state_modeler_params{ + .max_input_samples = 20, + .project_to_segment_along_normal_is_enabled = true}, + .prediction_params = StrokeEndPredictorParams()}; + +TEST(StrokeModelerWithNewProjectionTest, NoPredictionUponInit) { + StrokeModeler modeler; + ASSERT_TRUE(modeler.Reset(kDefaultParams).ok()); + std::vector results; + EXPECT_EQ(modeler.Predict(results).code(), + absl::StatusCode::kFailedPrecondition); +} + +TEST(StrokeModelerWithNewProjectionTest, NoPredictionWithDisabledPredictor) { + StrokeModeler modeler; + StrokeModelParams params = kDefaultParams; + params.prediction_params = DisabledPredictorParams{}; + EXPECT_TRUE(modeler.Reset(params).ok()); + std::vector results; + EXPECT_EQ(modeler.Predict(results).code(), + absl::StatusCode::kFailedPrecondition); +} + +TEST(StrokeModelerWithNewProjectionTest, InputRateSlowerThanMinOutputRate) { + const Duration kDeltaTime{1. / 30}; + + StrokeModeler modeler; + ASSERT_TRUE(modeler.Reset(kDefaultParams).ok()); + + Time time{0}; + std::vector results; + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kDown, + .position = {3, 4}, + .time = time}, + results) + .ok()); + EXPECT_THAT(results, ElementsAre(ResultNear( + {.position = {3, 4}, .time = Time(0)}, kTol))); + + ASSERT_TRUE(modeler.Predict(results).ok()); + EXPECT_THAT(results, IsEmpty()); + + time += kDeltaTime; + results.clear(); + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kMove, + .position = {3.2, 4.2}, + .time = time}, + results) + .ok()); + EXPECT_THAT(results, + ElementsAre(ResultNear({.position = {3.0019, 4.0019}, + .velocity = {0.4007, 0.4007}, + .acceleration = {84.1557, 84.1564}, + .time = Time(0.0048)}, + kTol), + ResultNear({.position = {3.0069, 4.0069}, + .velocity = {1.0381, 1.0381}, + .acceleration = {133.8378, 133.8369}, + .time = Time(0.0095)}, + kTol), + ResultNear({.position = {3.0154, 4.0154}, + .velocity = {1.7883, 1.7883}, + .acceleration = {157.5465, 157.5459}, + .time = Time(0.0143)}, + kTol), + ResultNear({.position = {3.0276, 4.0276}, + .velocity = {2.5626, 2.5626}, + .acceleration = {162.6039, 162.6021}, + .time = Time(0.0190)}, + kTol), + ResultNear({.position = {3.0433, 4.0433}, + .velocity = {3.3010, 3.3010}, + .acceleration = {155.0670, 155.0666}, + .time = Time(0.0238)}, + kTol), + ResultNear({.position = {3.0622, 4.0622}, + .velocity = {3.9665, 3.9665}, + .acceleration = {139.7575, 139.7564}, + .time = Time(0.0286)}, + kTol), + ResultNear({.position = {3.0838, 4.0838}, + .velocity = {4.5397, 4.5397}, + .acceleration = {120.3618, 120.3625}, + .time = Time(0.0333)}, + kTol))); + + ASSERT_TRUE(modeler.Predict(results).ok()); + EXPECT_THAT(results, + ElementsAre(ResultNear({.position = {3.1095, 4.1095}, + .velocity = {4.6253, 4.6253}, + .acceleration = {15.4218, 15.4223}, + .time = Time(0.0389)}, + kTol), + ResultNear({.position = {3.1331, 4.1331}, + .velocity = {4.2563, 4.2563}, + .acceleration = {-66.4341, -66.4339}, + .time = Time(0.0444)}, + kTol), + ResultNear({.position = {3.1534, 4.1534}, + .velocity = {3.6479, 3.6479}, + .acceleration = {-109.5083, -109.5081}, + .time = Time(0.0500)}, + kTol), + ResultNear({.position = {3.1698, 4.1698}, + .velocity = {2.9512, 2.9512}, + .acceleration = {-125.3978, -125.3976}, + .time = Time(0.0556)}, + kTol), + ResultNear({.position = {3.1824, 4.1824}, + .velocity = {2.2649, 2.2649}, + .acceleration = {-123.5318, -123.5310}, + .time = Time(0.0611)}, + kTol), + ResultNear({.position = {3.1915, 4.1915}, + .velocity = {1.6473, 1.6473}, + .acceleration = {-111.1818, -111.1806}, + .time = Time(0.0667)}, + kTol), + ResultNear({.position = {3.1978, 4.1978}, + .velocity = {1.1269, 1.1269}, + .acceleration = {-93.6643, -93.6636}, + .time = Time(0.0722)}, + kTol), + ResultNear({.position = {3.1992, 4.1992}, + .velocity = {1.0232, 1.0232}, + .acceleration = {-74.6390, -74.6392}, + .time = Time(0.0736)}, + kTol))); + + time += kDeltaTime; + results.clear(); + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kMove, + .position = {3.5, 4.2}, + .time = time}, + results) + .ok()); + EXPECT_THAT(results, + ElementsAre(ResultNear({.position = {3.1086, 4.1058}, + .velocity = {5.2142, 4.6131}, + .acceleration = {141.6557, 15.4223}, + .time = Time(0.0381)}, + kTol), + ResultNear({.position = {3.1368, 4.1265}, + .velocity = {5.9103, 4.3532}, + .acceleration = {146.1873, -54.5680}, + .time = Time(0.0429)}, + kTol), + ResultNear({.position = {3.1681, 4.1450}, + .velocity = {6.5742, 3.8917}, + .acceleration = {139.4012, -96.9169}, + .time = Time(0.0476)}, + kTol), + ResultNear({.position = {3.2022, 4.1609}, + .velocity = {7.1724, 3.3285}, + .acceleration = {125.6306, -118.2742}, + .time = Time(0.0524)}, + kTol), + ResultNear({.position = {3.2388, 4.1739}, + .velocity = {7.6876, 2.7361}, + .acceleration = {108.1908, -124.4087}, + .time = Time(0.0571)}, + kTol), + ResultNear({.position = {3.2775, 4.1842}, + .velocity = {8.1138, 2.1640}, + .acceleration = {89.5049, -120.1309}, + .time = Time(0.0619)}, + kTol), + ResultNear({.position = {3.3177, 4.1920}, + .velocity = {8.4531, 1.6436}, + .acceleration = {71.2473, -109.2959}, + .time = Time(0.0667)}, + kTol))); + + ASSERT_TRUE(modeler.Predict(results).ok()); + EXPECT_THAT(results, + ElementsAre(ResultNear({.position = {3.3625, 4.1982}, + .velocity = {8.0545, 1.1165}, + .acceleration = {-71.7427, -94.8765}, + .time = Time(0.0722)}, + kTol), + ResultNear({.position = {3.4018, 4.2021}, + .velocity = {7.0831, 0.6987}, + .acceleration = {-174.8469, -75.1957}, + .time = Time(0.0778)}, + kTol), + ResultNear({.position = {3.4344, 4.2043}, + .velocity = {5.8564, 0.3846}, + .acceleration = {-220.8140, -56.5515}, + .time = Time(0.0833)}, + kTol), + ResultNear({.position = {3.4598, 4.2052}, + .velocity = {4.5880, 0.1611}, + .acceleration = {-228.3204, -40.2244}, + .time = Time(0.0889)}, + kTol), + ResultNear({.position = {3.4788, 4.2052}, + .velocity = {3.4098, 0.0124}, + .acceleration = {-212.0678, -26.7709}, + .time = Time(0.0944)}, + kTol), + ResultNear({.position = {3.4921, 4.2048}, + .velocity = {2.3929, -0.0780}, + .acceleration = {-183.0373, -16.2648}, + .time = Time(0.1000)}, + kTol), + ResultNear({.position = {3.4976, 4.2045}, + .velocity = {1.9791, -0.1015}, + .acceleration = {-148.9792, -8.4822}, + .time = Time(0.1028)}, + kTol), + ResultNear({.position = {3.5001, 4.2044}, + .velocity = {1.7911, -0.1098}, + .acceleration = {-135.3759, -5.9543}, + .time = Time(0.1042)}, + kTol))); + + time += kDeltaTime; + // We get more results at the end of the stroke as it tries to "catch up" to + // the raw input. + results.clear(); + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kUp, + .position = {3.7, 4.4}, + .time = time}, + results) + .ok()); + EXPECT_THAT(results, + ElementsAre(ResultNear({.position = {3.3583, 4.1996}, + .velocity = {8.5122, 1.5925}, + .acceleration = {12.4129, -10.7201}, + .time = Time(0.0714)}, + kTol), + ResultNear({.position = {3.3982, 4.2084}, + .velocity = {8.3832, 1.8534}, + .acceleration = {-27.0783, 54.7731}, + .time = Time(0.0762)}, + kTol), + ResultNear({.position = {3.4369, 4.2194}, + .velocity = {8.1393, 2.3017}, + .acceleration = {-51.2222, 94.1542}, + .time = Time(0.0810)}, + kTol), + ResultNear({.position = {3.4743, 4.2329}, + .velocity = {7.8362, 2.8434}, + .acceleration = {-63.6668, 113.7452}, + .time = Time(0.0857)}, + kTol), + ResultNear({.position = {3.5100, 4.2492}, + .velocity = {7.5143, 3.4101}, + .acceleration = {-67.5926, 119.0224}, + .time = Time(0.0905)}, + kTol), + ResultNear({.position = {3.5443, 4.2680}, + .velocity = {7.2016, 3.9556}, + .acceleration = {-65.6568, 114.5394}, + .time = Time(0.0952)}, + kTol), + ResultNear({.position = {3.5773, 4.2892}, + .velocity = {6.9159, 4.4505}, + .acceleration = {-59.9999, 103.9444}, + .time = Time(0.1000)}, + kTol), + ResultNear({.position = {3.6115, 4.3141}, + .velocity = {6.1580, 4.4832}, + .acceleration = {-136.4312, 5.8833}, + .time = Time(0.1056)}, + kTol), + ResultNear({.position = {3.6400, 4.3369}, + .velocity = {5.1434, 4.0953}, + .acceleration = {-182.6254, -69.8314}, + .time = Time(0.1111)}, + kTol), + ResultNear({.position = {3.6626, 4.3563}, + .velocity = {4.0671, 3.4902}, + .acceleration = {-193.7401, -108.9119}, + .time = Time(0.1167)}, + kTol), + ResultNear({.position = {3.6796, 4.3719}, + .velocity = {3.0515, 2.8099}, + .acceleration = {-182.7957, -122.4598}, + .time = Time(0.1222)}, + kTol), + ResultNear({.position = {3.6916, 4.3838}, + .velocity = {2.1648, 2.1462}, + .acceleration = {-159.6116, -119.4551}, + .time = Time(0.1278)}, + kTol), + ResultNear({.position = {3.6996, 4.3924}, + .velocity = {1.4360, 1.5529}, + .acceleration = {-131.1906, -106.7926}, + .time = Time(0.1333)}, + kTol), + ResultNear({.position = {3.7028, 4.3960}, + .velocity = {1.1520, 1.3044}, + .acceleration = {-102.2117, -89.4872}, + .time = Time(0.1361)}, + kTol))); + + // The stroke is finished, so there's nothing to predict anymore. + EXPECT_EQ(modeler.Predict(results).code(), + absl::StatusCode::kFailedPrecondition); +} + +TEST(StrokeModelerWithNewProjectionTest, + InputRateSlowerThanMinOutputRateWithLoopMitigation) { + const Duration kDeltaTime{1. / 30}; + StrokeModelParams params = kDefaultParams; + params.position_modeler_params.loop_contraction_mitigation_params = + PositionModelerParams::LoopContractionMitigationParameters{ + .is_enabled = true, + .speed_lower_bound = 0, + .speed_upper_bound = 10, + .interpolation_strength_at_speed_lower_bound = 1, + .interpolation_strength_at_speed_upper_bound = 0, + .n_speed_samples = 10}; + + StrokeModeler modeler; + ASSERT_TRUE(modeler.Reset(params).ok()); + + Time time{0}; + std::vector results; + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kDown, + .position = {3, 4}, + .time = time}, + results) + .ok()); + EXPECT_THAT(results, ElementsAre(ResultNear( + {.position = {3, 4}, .time = Time(0)}, kTol))); + + ASSERT_TRUE(modeler.Predict(results).ok()); + EXPECT_THAT(results, IsEmpty()); + + time += kDeltaTime; + results.clear(); + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kMove, + .position = {3.2, 4.2}, + .time = time}, + results) + .ok()); + EXPECT_THAT(results, + ElementsAre(ResultNear({.position = {3.0019, 4.0019}, + .velocity = {0.4007, 0.4007}, + .acceleration = {84.1557, 84.1564}, + .time = Time(0.0047619)}, + kTol), + ResultNear({.position = {3.0069, 4.0069}, + .velocity = {0.9909, 0.9909}, + .acceleration = {126.6022, 126.6014}, + .time = Time(0.00952381)}, + kTol), + ResultNear({.position = {3.0154, 4.0154}, + .velocity = {1.6577, 1.6577}, + .acceleration = {143.4044, 143.4039}, + .time = Time(0.0142857)}, + kTol), + ResultNear({.position = {3.0276, 4.0276}, + .velocity = {2.3131, 2.3131}, + .acceleration = {142.7971, 142.7955}, + .time = Time(0.0190476)}, + kTol), + ResultNear({.position = {3.0433, 4.0433}, + .velocity = {2.9214, 2.9214}, + .acceleration = {133.0543, 133.0539}, + .time = Time(0.0238095)}, + kTol), + ResultNear({.position = {3.0622, 4.0622}, + .velocity = {3.4742, 3.4742}, + .acceleration = {120.1235, 120.1227}, + .time = Time(0.0285714)}, + kTol), + ResultNear({.position = {3.0838, 4.0838}, + .velocity = {3.9782, 3.9782}, + .acceleration = {107.9054, 107.9059}, + .time = Time(0.0333333)}, + kTol))); + + ASSERT_TRUE(modeler.Predict(results).ok()); + EXPECT_THAT(results, + ElementsAre(ResultNear({.position = {3.1095, 4.1095}, + .velocity = {4.1991, 4.1991}, + .acceleration = {41.8473, 41.8476}, + .time = Time(0.0389)}, + kTol), + ResultNear({.position = {3.1331, 4.1331}, + .velocity = {4.1639, 4.1639}, + .acceleration = {-0.7951, -0.7950}, + .time = Time(0.0444)}, + kTol), + ResultNear({.position = {3.1534, 4.1534}, + .velocity = {4.0092, 4.0092}, + .acceleration = {-15.7575, -15.7574}, + .time = Time(0.0500)}, + kTol), + ResultNear({.position = {3.1698, 4.1698}, + .velocity = {3.8030, 3.8030}, + .acceleration = {-14.803, -14.803}, + .time = Time(0.0556)}, + kTol), + ResultNear({.position = {3.1824, 4.1824}, + .velocity = {3.6939, 3.6939}, + .acceleration = {4.6644, 4.6648}, + .time = Time(0.0611)}, + kTol), + ResultNear({.position = {3.1915, 4.1915}, + .velocity = {3.6305, 3.6305}, + .acceleration = {26.0206, 26.0211}, + .time = Time(0.0667)}, + kTol), + ResultNear({.position = {3.1978, 4.1978}, + .velocity = {3.5869, 3.5869}, + .acceleration = {45.3682, 45.3684}, + .time = Time(0.0722)}, + kTol), + ResultNear({.position = {3.1992, 4.1992}, + .velocity = {3.6473, 3.6473}, + .acceleration = {59.8859, 59.8857}, + .time = Time(0.0736)}, + kTol))); + + time += kDeltaTime; + results.clear(); + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kMove, + .position = {3.5, 4.2}, + .time = time}, + results) + .ok()); + EXPECT_THAT(results, + ElementsAre(ResultNear({.position = {3.1081, 4.1061}, + .velocity = {4.5757, 4.1657}, + .acceleration = {127.1973, 41.0960}, + .time = Time(0.0380952)}, + kTol), + ResultNear({.position = {3.1353, 4.1287}, + .velocity = {5.2236, 4.2201}, + .acceleration = {136.6492, 7.2682}, + .time = Time(0.0428571)}, + kTol), + ResultNear({.position = {3.1649, 4.1509}, + .velocity = {5.8820, 4.2474}, + .acceleration = {141.2239, -2.7790}, + .time = Time(0.047619)}, + kTol), + ResultNear({.position = {3.1971, 4.1733}, + .velocity = {6.5508, 4.3372}, + .acceleration = {144.9591, 4.5010}, + .time = Time(0.052381)}, + kTol), + ResultNear({.position = {3.2348, 4.1869}, + .velocity = {7.0019, 4.0522}, + .acceleration = {139.2895, 8.5209}, + .time = Time(0.0571429)}, + kTol), + ResultNear({.position = {3.2755, 4.1931}, + .velocity = {7.3385, 3.4937}, + .acceleration = {128.0340, -0.8921}, + .time = Time(0.0619048)}, + kTol), + ResultNear({.position = {3.3169, 4.1970}, + .velocity = {7.6519, 2.9042}, + .acceleration = {117.1442, -16.3060}, + .time = Time(0.0666667)}, + kTol))); + + ASSERT_TRUE(modeler.Predict(results).ok()); + EXPECT_THAT(results, + ElementsAre(ResultNear({.position = {3.3623, 4.1994}, + .velocity = {7.7649, 2.2153}, + .acceleration = {64.4841, -41.1149}, + .time = Time(0.0722222)}, + kTol), + ResultNear({.position = {3.4019, 4.2006}, + .velocity = {7.7484, 1.5949}, + .acceleration = {34.1646, -66.0874}, + .time = Time(0.0777778)}, + kTol), + ResultNear({.position = {3.4345, 4.2011}, + .velocity = {7.6989, 1.0692}, + .acceleration = {23.8171, -89.7804}, + .time = Time(0.0833333)}, + kTol), + ResultNear({.position = {3.4599, 4.2012}, + .velocity = {7.6433, 0.6495}, + .acceleration = {23.3276, -110.0346}, + .time = Time(0.0888889)}, + kTol), + ResultNear({.position = {3.4787, 4.2012}, + .velocity = {7.5852, 0.3345}, + .acceleration = {27.4889, -125.8624}, + .time = Time(0.0944444)}, + kTol), + ResultNear({.position = {3.4918, 4.2010}, + .velocity = {7.5168, 0.1134}, + .acceleration = {33.3461, -137.0478}, + .time = Time(0.1)}, + kTol), + ResultNear({.position = {3.4974, 4.2010}, + .velocity = {7.4901, 0.0212}, + .acceleration = {39.9729, -141.0700}, + .time = Time(0.102778)}, + kTol), + ResultNear({.position = {3.4998, 4.2009}, + .velocity = {7.4434, -0.0202}, + .acceleration = {41.4426, -142.2514}, + .time = Time(0.104167)}, + kTol))); + + time += kDeltaTime; + // We get more results at the end of the stroke as it tries to "catch up" to + // the raw input. + results.clear(); + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kUp, + .position = {3.7, 4.4}, + .time = time}, + results) + .ok()); + EXPECT_THAT(results, + ElementsAre(ResultNear({.position = {3.3582, 4.1999}, + .velocity = {7.8881, 2.4269}, + .acceleration = {93.0054, -10.1219}, + .time = Time(0.0714286)}, + kTol), + ResultNear({.position = {3.3996, 4.2024}, + .velocity = {8.1117, 1.9550}, + .acceleration = {77.6135, -27.1225}, + .time = Time(0.0761905)}, + kTol), + ResultNear({.position = {3.4415, 4.2049}, + .velocity = {8.3567, 1.4338}, + .acceleration = {66.9368, -59.4714}, + .time = Time(0.0809524)}, + kTol), + ResultNear({.position = {3.4846, 4.2074}, + .velocity = {8.6421, 0.8320}, + .acceleration = {58.2938, -102.3662}, + .time = Time(0.0857143)}, + kTol), + ResultNear({.position = {3.5205, 4.2283}, + .velocity = {8.4252, 1.2374}, + .acceleration = {41.8119, -86.8585}, + .time = Time(0.0904762)}, + kTol), + ResultNear({.position = {3.5516, 4.2559}, + .velocity = {8.0196, 2.0261}, + .acceleration = {22.4800, -48.0150}, + .time = Time(0.0952381)}, + kTol), + ResultNear({.position = {3.5813, 4.2834}, + .velocity = {7.6212, 2.8062}, + .acceleration = {2.9799, -8.7206}, + .time = Time(0.1)}, + kTol), + ResultNear({.position = {3.6124, 4.3129}, + .velocity = {7.1154, 3.5660}, + .acceleration = {-32.5449, 19.8864}, + .time = Time(0.105556)}, + kTol), + ResultNear({.position = {3.6388, 4.3383}, + .velocity = {6.6178, 4.1471}, + .acceleration = {-59.9670, 45.6982}, + .time = Time(0.111111)}, + kTol), + ResultNear({.position = {3.6601, 4.3590}, + .velocity = {6.1647, 4.5625}, + .acceleration = {-77.9914, 69.7081}, + .time = Time(0.116667)}, + kTol), + ResultNear({.position = {3.6765, 4.3751}, + .velocity = {5.7708, 4.8323}, + .acceleration = {-88.7012, 90.0637}, + .time = Time(0.122222)}, + kTol), + ResultNear({.position = {3.6883, 4.3869}, + .velocity = {5.4375, 4.9790}, + .acceleration = {-93.8602, 106.0317}, + .time = Time(0.127778)}, + kTol), + ResultNear({.position = {3.6964, 4.3950}, + .velocity = {5.1580, 5.0242}, + .acceleration = {-94.9365, 117.5414}, + .time = Time(0.133333)}, + kTol), + ResultNear({.position = {3.6999, 4.3985}, + .velocity = {4.9982, 4.9999}, + .acceleration = {-91.9452, 122.5481}, + .time = Time(0.136111)}, + kTol))); + + // The stroke is finished, so there's nothing to predict anymore. + EXPECT_EQ(modeler.Predict(results).code(), + absl::StatusCode::kFailedPrecondition); +} + +TEST(StrokeModelerWithNewProjectionTest, InputRateFasterThanMinOutputRate) { + const Duration kDeltaTime{1. / 300}; + + StrokeModeler modeler; + ASSERT_TRUE(modeler.Reset(kDefaultParams).ok()); + + Time time{2}; + std::vector results; + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kDown, + .position = {5, -3}, + .time = time}, + results) + .ok()); + EXPECT_THAT(results, ElementsAre(ResultNear( + {.position = {5, -3}, .time = Time(2)}, kTol))); + + ASSERT_TRUE(modeler.Predict(results).ok()); + EXPECT_THAT(results, IsEmpty()); + + time += kDeltaTime; + results.clear(); + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kMove, + .position = {5, -3.1}, + .time = time}, + results) + .ok()); + EXPECT_THAT(results, ElementsAre(ResultNear({.position = {5, -3.0033}, + .velocity = {0, -0.9818}, + .acceleration = {0, -294.5452}, + .time = Time(2.0033)}, + kTol))); + + ASSERT_TRUE(modeler.Predict(results).ok()); + EXPECT_THAT(results, ElementsAre(ResultNear({.position = {5, -3.0153}, + .velocity = {0, -2.1719}, + .acceleration = {0, -214.2145}, + .time = Time(2.0089)}, + kTol), + ResultNear({.position = {5, -3.0303}, + .velocity = {0, -2.6885}, + .acceleration = {0, -92.9885}, + .time = Time(2.0144)}, + kTol), + ResultNear({.position = {5, -3.0456}, + .velocity = {0, -2.7541}, + .acceleration = {0, -11.7991}, + .time = Time(2.0200)}, + kTol), + ResultNear({.position = {5, -3.0597}, + .velocity = {0, -2.5430}, + .acceleration = {0, 37.9868}, + .time = Time(2.0256)}, + kTol), + ResultNear({.position = {5, -3.0718}, + .velocity = {0, -2.1852}, + .acceleration = {0, 64.4053}, + .time = Time(2.0311)}, + kTol), + ResultNear({.position = {5, -3.0817}, + .velocity = {0, -1.7719}, + .acceleration = {0, 74.4014}, + .time = Time(2.0367)}, + kTol), + ResultNear({.position = {5, -3.0893}, + .velocity = {0, -1.3628}, + .acceleration = {0, 73.6348}, + .time = Time(2.0422)}, + kTol), + ResultNear({.position = {5, -3.0948}, + .velocity = {0, -0.9934}, + .acceleration = {0, 66.4805}, + .time = Time(2.0478)}, + kTol), + ResultNear({.position = {5, -3.0986}, + .velocity = {0, -0.6815}, + .acceleration = {0, 56.1445}, + .time = Time(2.0533)}, + kTol))); + + time += kDeltaTime; + results.clear(); + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kMove, + .position = {4.975, -3.175}, + .time = time}, + results) + .ok()); + EXPECT_THAT(results, + ElementsAre(ResultNear({.position = {4.9992, -3.0114}, + .velocity = {-0.2455, -2.4322}, + .acceleration = {-73.6366, -435.1238}, + .time = Time(2.0067)}, + kTol))); + + ASSERT_TRUE(modeler.Predict(results).ok()); + EXPECT_THAT(results, + ElementsAre(ResultNear({.position = {4.9962, -3.0344}, + .velocity = {-0.5430, -4.1368}, + .acceleration = {-53.5537, -306.8140}, + .time = Time(2.0122)}, + kTol), + ResultNear({.position = {4.9924, -3.0609}, + .velocity = {-0.6721, -4.7834}, + .acceleration = {-23.2474, -116.3965}, + .time = Time(2.0178)}, + kTol), + ResultNear({.position = {4.9886, -3.0873}, + .velocity = {-0.6885, -4.7365}, + .acceleration = {-2.9498, 8.4355}, + .time = Time(2.0233)}, + kTol), + ResultNear({.position = {4.9851, -3.1110}, + .velocity = {-0.6358, -4.2778}, + .acceleration = {9.4971, 82.5684}, + .time = Time(2.0289)}, + kTol), + ResultNear({.position = {4.9820, -3.1311}, + .velocity = {-0.5463, -3.6137}, + .acceleration = {16.1014, 119.5415}, + .time = Time(2.0344)}, + kTol), + ResultNear({.position = {4.9796, -3.1471}, + .velocity = {-0.4430, -2.8867}, + .acceleration = {18.6005, 130.8579}, + .time = Time(2.0400)}, + kTol), + ResultNear({.position = {4.9777, -3.1593}, + .velocity = {-0.3407, -2.1881}, + .acceleration = {18.4089, 125.7516}, + .time = Time(2.0456)}, + kTol), + ResultNear({.position = {4.9763, -3.1680}, + .velocity = {-0.2484, -1.5700}, + .acceleration = {16.6198, 111.2560}, + .time = Time(2.0511)}, + kTol), + ResultNear({.position = {4.9754, -3.1739}, + .velocity = {-0.1704, -1.0564}, + .acceleration = {14.0366, 92.4447}, + .time = Time(2.0567)}, + kTol))); + + time += kDeltaTime; + results.clear(); + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kMove, + .position = {4.9, -3.2}, + .time = time}, + results) + .ok()); + EXPECT_THAT(results, + ElementsAre(ResultNear({.position = {4.9953, -3.0237}, + .velocity = {-1.1603, -3.7004}, + .acceleration = {-274.4622, -380.4507}, + .time = Time(2.0100)}, + kTol))); + + ASSERT_TRUE(modeler.Predict(results).ok()); + EXPECT_THAT(results, + ElementsAre(ResultNear({.position = {4.9828, -3.0521}, + .velocity = {-2.2559, -5.1049}, + .acceleration = {-197.1994, -252.8115}, + .time = Time(2.0156)}, + kTol), + ResultNear({.position = {4.9677, -3.0825}, + .velocity = {-2.7081, -5.4835}, + .acceleration = {-81.4051, -68.1523}, + .time = Time(2.0211)}, + kTol), + ResultNear({.position = {4.9526, -3.1115}, + .velocity = {-2.7333, -5.2122}, + .acceleration = {-4.5282, 48.8394}, + .time = Time(2.0267)}, + kTol), + ResultNear({.position = {4.9387, -3.1369}, + .velocity = {-2.4999, -4.5756}, + .acceleration = {42.0094, 114.5943}, + .time = Time(2.0322)}, + kTol), + ResultNear({.position = {4.9268, -3.1579}, + .velocity = {-2.1326, -3.7776}, + .acceleration = {66.1132, 143.6292}, + .time = Time(2.0378)}, + kTol), + ResultNear({.position = {4.9173, -3.1743}, + .velocity = {-1.7184, -2.9554}, + .acceleration = {74.5656, 147.9932}, + .time = Time(2.0433)}, + kTol), + ResultNear({.position = {4.9100, -3.1865}, + .velocity = {-1.3136, -2.1935}, + .acceleration = {72.8575, 137.1578}, + .time = Time(2.0489)}, + kTol), + ResultNear({.position = {4.9047, -3.1950}, + .velocity = {-0.9513, -1.5369}, + .acceleration = {65.2090, 118.1874}, + .time = Time(2.0544)}, + kTol), + ResultNear({.position = {4.9011, -3.2006}, + .velocity = {-0.6475, -1.0032}, + .acceleration = {54.6929, 96.0605}, + .time = Time(2.0600)}, + kTol))); + + time += kDeltaTime; + results.clear(); + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kMove, + .position = {4.825, -3.2}, + .time = time}, + results) + .ok()); + EXPECT_THAT(results, + ElementsAre(ResultNear({.position = {4.9868, -3.0389}, + .velocity = {-2.5540, -4.5431}, + .acceleration = {-418.1093, -252.8115}, + .time = Time(2.0133)}, + kTol))); + + ASSERT_TRUE(modeler.Predict(results).ok()); + EXPECT_THAT(results, + ElementsAre(ResultNear({.position = {4.9636, -3.0687}, + .velocity = {-4.1801, -5.3627}, + .acceleration = {-292.6871, -147.5322}, + .time = Time(2.0189)}, + kTol), + ResultNear({.position = {4.9370, -3.0985}, + .velocity = {-4.7757, -5.3670}, + .acceleration = {-107.2116, -0.7651}, + .time = Time(2.0244)}, + kTol), + ResultNear({.position = {4.9109, -3.1256}, + .velocity = {-4.6989, -4.8816}, + .acceleration = {13.8210, 87.3644}, + .time = Time(2.0300)}, + kTol), + ResultNear({.position = {4.8875, -3.1486}, + .velocity = {-4.2257, -4.1466}, + .acceleration = {85.1833, 132.2998}, + .time = Time(2.0356)}, + kTol), + ResultNear({.position = {4.8677, -3.1671}, + .velocity = {-3.5576, -3.3287}, + .acceleration = {120.2578, 147.2334}, + .time = Time(2.0411)}, + kTol), + ResultNear({.position = {4.8520, -3.1812}, + .velocity = {-2.8333, -2.5353}, + .acceleration = {130.3699, 142.8088}, + .time = Time(2.0467)}, + kTol), + ResultNear({.position = {4.8401, -3.1914}, + .velocity = {-2.1411, -1.8288}, + .acceleration = {124.5846, 127.1714}, + .time = Time(2.0522)}, + kTol), + ResultNear({.position = {4.8316, -3.1982}, + .velocity = {-1.5312, -1.2386}, + .acceleration = {109.7874, 106.2278}, + .time = Time(2.0578)}, + kTol), + ResultNear({.position = {4.8280, -3.2010}, + .velocity = {-1.2786, -1.0053}, + .acceleration = {90.9288, 84.0051}, + .time = Time(2.0606)}, + kTol), + ResultNear({.position = {4.8272, -3.2017}, + .velocity = {-1.2209, -0.9529}, + .acceleration = {83.2052, 75.429}, + .time = Time(2.0613)}, + kTol))); + + time += kDeltaTime; + results.clear(); + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kMove, + .position = {4.75, -3.225}, + .time = time}, + results) + .ok()); + EXPECT_THAT(results, + ElementsAre(ResultNear({.position = {4.9726, -3.0565}, + .velocity = {-4.2660, -5.2803}, + .acceleration = {-513.5957, -221.168}, + .time = Time(2.0167)}, + kTol))); + + ASSERT_TRUE(modeler.Predict(results).ok()); + EXPECT_THAT(results, + ElementsAre(ResultNear({.position = {4.9381, -3.0894}, + .velocity = {-6.2018, -5.9261}, + .acceleration = {-348.4476, -116.2446}, + .time = Time(2.0222)}, + kTol), + ResultNear({.position = {4.9004, -3.1215}, + .velocity = {-6.7995, -5.7749}, + .acceleration = {-107.5834, 27.2264}, + .time = Time(2.0278)}, + kTol), + ResultNear({.position = {4.8640, -3.1501}, + .velocity = {-6.5400, -5.1591}, + .acceleration = {46.7144, 110.8335}, + .time = Time(2.0333)}, + kTol), + ResultNear({.position = {4.8319, -3.1741}, + .velocity = {-5.7897, -4.3207}, + .acceleration = {135.0462, 150.9226}, + .time = Time(2.0389)}, + kTol), + ResultNear({.position = {4.8051, -3.1932}, + .velocity = {-4.8132, -3.4248}, + .acceleration = {175.7684, 161.2555}, + .time = Time(2.0444)}, + kTol), + ResultNear({.position = {4.7841, -3.2075}, + .velocity = {-3.7898, -2.5759}, + .acceleration = {184.2227, 152.7958}, + .time = Time(2.0500)}, + kTol), + ResultNear({.position = {4.7683, -3.2176}, + .velocity = {-2.8312, -1.8324}, + .acceleration = {172.5480, 133.8294}, + .time = Time(2.0556)}, + kTol), + ResultNear({.position = {4.7572, -3.2244}, + .velocity = {-1.9986, -1.2198}, + .acceleration = {149.8577, 110.2830}, + .time = Time(2.0611)}, + kTol), + ResultNear({.position = {4.7526, -3.2271}, + .velocity = {-1.6580, -0.9805}, + .acceleration = {122.6198, 86.1299}, + .time = Time(2.0639)}, + kTol))); + + time += kDeltaTime; + results.clear(); + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kMove, + .position = {4.7, -3.3}, + .time = time}, + results) + .ok()); + EXPECT_THAT(results, + ElementsAre(ResultNear({.position = {4.9529, -3.0778}, + .velocity = {-5.9184, -6.4042}, + .acceleration = {-495.7209, -337.1538}, + .time = Time(2.0200)}, + kTol))); + + ASSERT_TRUE(modeler.Predict(results).ok()); + EXPECT_THAT(results, + ElementsAre(ResultNear({.position = {4.9101, -3.1194}, + .velocity = {-7.6886, -7.4784}, + .acceleration = {-318.6394, -193.3594}, + .time = Time(2.0256)}, + kTol), + ResultNear({.position = {4.8654, -3.1607}, + .velocity = {-8.0518, -7.4431}, + .acceleration = {-65.3696, 6.3579}, + .time = Time(2.0311)}, + kTol), + ResultNear({.position = {4.8235, -3.1982}, + .velocity = {-7.5377, -6.7452}, + .acceleration = {92.5345, 125.6104}, + .time = Time(2.0367)}, + kTol), + ResultNear({.position = {4.7872, -3.2299}, + .velocity = {-6.5440, -5.7133}, + .acceleration = {178.8654, 185.7426}, + .time = Time(2.0422)}, + kTol), + ResultNear({.position = {4.7574, -3.2553}, + .velocity = {-5.3529, -4.5748}, + .acceleration = {214.4027, 204.9362}, + .time = Time(2.0478)}, + kTol), + ResultNear({.position = {4.7344, -3.2746}, + .velocity = {-4.1516, -3.4758}, + .acceleration = {216.2348, 197.8225}, + .time = Time(2.0533)}, + kTol), + ResultNear({.position = {4.7174, -3.2885}, + .velocity = {-3.0534, -2.5004}, + .acceleration = {197.6767, 175.5703}, + .time = Time(2.0589)}, + kTol), + ResultNear({.position = {4.7056, -3.2979}, + .velocity = {-2.1169, -1.6879}, + .acceleration = {168.5711, 146.2573}, + .time = Time(2.0644)}, + kTol), + ResultNear({.position = {4.7030, -3.3000}, + .velocity = {-1.9283, -1.5276}, + .acceleration = {135.7820, 115.3740}, + .time = Time(2.0658)}, + kTol), + ResultNear({.position = {4.7017, -3.3010}, + .velocity = {-1.8380, -1.4512}, + .acceleration = {130.0928, 110.0859}, + .time = Time(2.0665)}, + kTol))); + + time += kDeltaTime; + results.clear(); + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kMove, + .position = {4.675, -3.4}, + .time = time}, + results) + .ok()); + EXPECT_THAT(results, + ElementsAre(ResultNear({.position = {4.9288, -3.1046}, + .velocity = {-7.2260, -8.0305}, + .acceleration = {-392.2747, -487.9053}, + .time = Time(2.0233)}, + kTol))); + + ASSERT_TRUE(modeler.Predict(results).ok()); + EXPECT_THAT(results, + ElementsAre(ResultNear({.position = {4.8816, -3.1582}, + .velocity = {-8.4881, -9.6525}, + .acceleration = {-227.1831, -291.9628}, + .time = Time(2.0289)}, + kTol), + ResultNear({.position = {4.8345, -3.2124}, + .velocity = {-8.4738, -9.7482}, + .acceleration = {2.5870, -17.2266}, + .time = Time(2.0344)}, + kTol), + ResultNear({.position = {4.7918, -3.2619}, + .velocity = {-7.6948, -8.9195}, + .acceleration = {140.2131, 149.1810}, + .time = Time(2.0400)}, + kTol), + ResultNear({.position = {4.7555, -3.3042}, + .velocity = {-6.5279, -7.6113}, + .acceleration = {210.0428, 235.4638}, + .time = Time(2.0456)}, + kTol), + ResultNear({.position = {4.7264, -3.3383}, + .velocity = {-5.2343, -6.1345}, + .acceleration = {232.8451, 265.8272}, + .time = Time(2.0511)}, + kTol), + ResultNear({.position = {4.7043, -3.3643}, + .velocity = {-3.9823, -4.6907}, + .acceleration = {225.3593, 259.8790}, + .time = Time(2.0567)}, + kTol), + ResultNear({.position = {4.6884, -3.3832}, + .velocity = {-2.8691, -3.3980}, + .acceleration = {200.3802, 232.6849}, + .time = Time(2.0622)}, + kTol), + ResultNear({.position = {4.6776, -3.3961}, + .velocity = {-1.9403, -2.3135}, + .acceleration = {167.1765, 195.2151}, + .time = Time(2.0678)}, + kTol), + ResultNear({.position = {4.6752, -3.3990}, + .velocity = {-1.7569, -2.0983}, + .acceleration = {132.0562, 154.9868}, + .time = Time(2.0692)}, + kTol))); + + time += kDeltaTime; + results.clear(); + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kMove, + .position = {4.675, -3.525}, + .time = time}, + results) + .ok()); + EXPECT_THAT(results, + ElementsAre(ResultNear({.position = {4.9022, -3.1387}, + .velocity = {-7.9833, -10.2310}, + .acceleration = {-227.1831, -660.1445}, + .time = Time(2.0267)}, + kTol))); + + ASSERT_TRUE(modeler.Predict(results).ok()); + EXPECT_THAT(results, + ElementsAre(ResultNear({.position = {4.8549, -3.2079}, + .velocity = {-8.5070, -12.4602}, + .acceleration = {-94.2781, -401.2598}, + .time = Time(2.0322)}, + kTol), + ResultNear({.position = {4.8102, -3.2783}, + .velocity = {-8.0479, -12.6650}, + .acceleration = {82.6390, -36.8616}, + .time = Time(2.0378)}, + kTol), + ResultNear({.position = {4.7711, -3.3429}, + .velocity = {-7.0408, -11.6365}, + .acceleration = {181.2766, 185.1284}, + .time = Time(2.0433)}, + kTol), + ResultNear({.position = {4.7389, -3.3983}, + .velocity = {-5.7965, -9.9616}, + .acceleration = {223.9801, 301.4932}, + .time = Time(2.0489)}, + kTol), + ResultNear({.position = {4.7137, -3.4430}, + .velocity = {-4.5230, -8.0510}, + .acceleration = {229.2397, 343.9031}, + .time = Time(2.0544)}, + kTol), + ResultNear({.position = {4.6951, -3.4773}, + .velocity = {-3.3477, -6.1727}, + .acceleration = {211.5554, 338.0856}, + .time = Time(2.0600)}, + kTol), + ResultNear({.position = {4.6821, -3.5022}, + .velocity = {-2.3381, -4.4846}, + .acceleration = {181.7131, 303.8597}, + .time = Time(2.0656)}, + kTol), + ResultNear({.position = {4.6737, -3.5192}, + .velocity = {-1.5199, -3.0641}, + .acceleration = {147.2879, 255.7004}, + .time = Time(2.0711)}, + kTol), + ResultNear({.position = {4.6718, -3.5231}, + .velocity = {-1.3626, -2.7813}, + .acceleration = {113.2437, 203.5595}, + .time = Time(2.0725)}, + kTol))); + + time += kDeltaTime; + // We get more results at the end of the stroke as it tries to "catch up" to + // the raw input. + results.clear(); + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kUp, + .position = {4.7, -3.6}, + .time = time}, + results) + .ok()); + EXPECT_THAT(results, + ElementsAre(ResultNear({.position = {4.8753, -3.1797}, + .velocity = {-8.0521, -12.3049}, + .acceleration = {-20.6431, -622.1685}, + .time = Time(2.0300)}, + kTol), + ResultNear({.position = {4.8325, -3.2589}, + .velocity = {-7.7000, -14.2607}, + .acceleration = {63.3680, -352.0363}, + .time = Time(2.0356)}, + kTol), + ResultNear({.position = {4.7948, -3.3375}, + .velocity = {-6.7888, -14.1377}, + .acceleration = {164.0215, 22.1348}, + .time = Time(2.0411)}, + kTol), + ResultNear({.position = {4.7636, -3.4085}, + .velocity = {-5.6249, -12.7787}, + .acceleration = {209.5020, 244.625}, + .time = Time(2.0467)}, + kTol), + ResultNear({.position = {4.7390, -3.4685}, + .velocity = {-4.4152, -10.8015}, + .acceleration = {217.7452, 355.8801}, + .time = Time(2.0522)}, + kTol), + ResultNear({.position = {4.7208, -3.5164}, + .velocity = {-3.2880, -8.6333}, + .acceleration = {202.8961, 390.2803}, + .time = Time(2.0578)}, + kTol), + ResultNear({.position = {4.7079, -3.5528}, + .velocity = {-2.3128, -6.5475}, + .acceleration = {175.5413, 375.4407}, + .time = Time(2.0633)}, + kTol), + ResultNear({.position = {4.6995, -3.5789}, + .velocity = {-1.5174, -4.7008}, + .acceleration = {143.1705, 332.4062}, + .time = Time(2.0689)}, + kTol), + ResultNear({.position = {4.6945, -3.5965}, + .velocity = {-0.9022, -3.1655}, + .acceleration = {110.7325, 276.3669}, + .time = Time(2.0744)}, + kTol), + ResultNear({.position = {4.6942, -3.5976}, + .velocity = {-0.8740, -3.0899}, + .acceleration = {81.2036, 217.6187}, + .time = Time(2.0748)}, + kTol))); + + // The stroke is finished, so there's nothing to predict anymore. + EXPECT_EQ(modeler.Predict(results).code(), + absl::StatusCode::kFailedPrecondition); +} + +TEST(StrokeModelerWithNewProjectionTest, WobbleSmoothed) { + const Duration kDeltaTime{.0167}; + + StrokeModeler modeler; + ASSERT_TRUE(modeler.Reset(kDefaultParams).ok()); + + Time time{4}; + std::vector results; + + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kDown, + .position = {-6, -2}, + .time = time}, + results) + .ok()); + EXPECT_THAT(results, ElementsAre(ResultNear( + {.position = {-6, -2}, .time = Time(4)}, kTol))); + + time += kDeltaTime; + results.clear(); + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kMove, + .position = {-6.02, -2}, + .time = time}, + results) + .ok()); + EXPECT_THAT(results, ElementsAre(ResultNear({.position = {-6.0003, -2}, + .velocity = {-0.0615, 0}, + .acceleration = {-14.7276, 0}, + .time = Time(4.0042)}, + kTol), + ResultNear({.position = {-6.0009, -2}, + .velocity = {-0.1628, 0}, + .acceleration = {-24.2725, 0}, + .time = Time(4.0084)}, + kTol), + ResultNear({.position = {-6.0021, -2}, + .velocity = {-0.2868, 0}, + .acceleration = {-29.6996, 0}, + .time = Time(4.0125)}, + kTol), + ResultNear({.position = {-6.0039, -2}, + .velocity = {-0.4203, 0}, + .acceleration = {-31.9728, 0}, + .time = Time(4.0167)}, + kTol))); + + time += kDeltaTime; + results.clear(); + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kMove, + .position = {-6.02, -2.02}, + .time = time}, + results) + .ok()); + EXPECT_THAT(results, + ElementsAre(ResultNear({.position = {-6.0059, -2.0001}, + .velocity = {-0.4921, -0.0307}, + .acceleration = {-17.1932, -7.3638}, + .time = Time(4.0209)}, + kTol), + ResultNear({.position = {-6.0081, -2.0005}, + .velocity = {-0.5170, -0.0814}, + .acceleration = {-5.9729, -12.1355}, + .time = Time(4.0251)}, + kTol), + ResultNear({.position = {-6.0102, -2.0010}, + .velocity = {-0.5079, -0.1434}, + .acceleration = {2.1807, -14.8493}, + .time = Time(4.0292)}, + kTol), + ResultNear({.position = {-6.0122, -2.0019}, + .velocity = {-0.4755, -0.2101}, + .acceleration = {7.7710, -15.9860}, + .time = Time(4.0334)}, + kTol))); + + time += kDeltaTime; + results.clear(); + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kMove, + .position = {-6.04, -2.02}, + .time = time}, + results) + .ok()); + EXPECT_THAT(results, + ElementsAre(ResultNear({.position = {-6.0141, -2.0030}, + .velocity = {-0.4489, -0.2563}, + .acceleration = {6.3733, -11.0507}, + .time = Time(4.0376)}, + kTol), + ResultNear({.position = {-6.0159, -2.0042}, + .velocity = {-0.4277, -0.2856}, + .acceleration = {5.0670, -7.0315}, + .time = Time(4.0418)}, + kTol), + ResultNear({.position = {-6.0176, -2.0055}, + .velocity = {-0.4115, -0.3018}, + .acceleration = {3.8950, -3.8603}, + .time = Time(4.0459)}, + kTol), + ResultNear({.position = {-6.0193, -2.0067}, + .velocity = {-0.3994, -0.3078}, + .acceleration = {2.8758, -1.4435}, + .time = Time(4.0501)}, + kTol))); + + time += kDeltaTime; + results.clear(); + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kMove, + .position = {-6.04, -2.04}, + .time = time}, + results) + .ok()); + EXPECT_THAT(results, + ElementsAre(ResultNear({.position = {-6.0209, -2.0082}, + .velocity = {-0.3910, -0.3372}, + .acceleration = {2.0142, -7.0427}, + .time = Time(4.0543)}, + kTol), + ResultNear({.position = {-6.0225, -2.0098}, + .velocity = {-0.3856, -0.3814}, + .acceleration = {1.3090, -10.5977}, + .time = Time(4.0585)}, + kTol), + ResultNear({.position = {-6.0241, -2.0116}, + .velocity = {-0.3825, -0.4338}, + .acceleration = {0.7470, -12.5399}, + .time = Time(4.0626)}, + kTol), + ResultNear({.position = {-6.0257, -2.0136}, + .velocity = {-0.3811, -0.4891}, + .acceleration = {0.3174, -13.2543}, + .time = Time(4.0668)}, + kTol))); +} + +TEST(StrokeModelerWithNewProjectionTest, WobbleNotSmoothedIfNotEnabled) { + const Duration kDeltaTime{.0167}; + + StrokeModelParams params = kDefaultParams; + params.wobble_smoother_params.is_enabled = false; + StrokeModeler modeler; + ASSERT_TRUE(modeler.Reset(params).ok()); + + Time time{4}; + std::vector results; + + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kDown, + .position = {-6, -2}, + .time = time}, + results) + .ok()); + EXPECT_THAT(results, ElementsAre(ResultNear( + {.position = {-6, -2}, .time = Time(4)}, kTol))); + + time += kDeltaTime; + results.clear(); + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kMove, + .position = {-6.02, -2}, + .time = time}, + results) + .ok()); + EXPECT_THAT(results, ElementsAre(ResultNear({.position = {-6.0003, -2}, + .velocity = {-0.0615, 0}, + .acceleration = {-14.7276, 0}, + .time = Time(4.0042)}, + kTol), + ResultNear({.position = {-6.0009, -2}, + .velocity = {-0.1628, 0}, + .acceleration = {-24.2725, 0}, + .time = Time(4.0084)}, + kTol), + ResultNear({.position = {-6.0021, -2}, + .velocity = {-0.2868, 0}, + .acceleration = {-29.6996, 0}, + .time = Time(4.0125)}, + kTol), + ResultNear({.position = {-6.0039, -2}, + .velocity = {-0.4203, 0}, + .acceleration = {-31.9728, 0}, + .time = Time(4.0167)}, + kTol))); + + time += kDeltaTime; + results.clear(); + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kMove, + .position = {-6.02, -2.02}, + .time = time}, + results) + .ok()); + EXPECT_THAT(results, + ElementsAre(ResultNear({.position = {-6.0059, -2.0003}, + .velocity = {-0.4921, -0.0615}, + .acceleration = {-17.1932, -14.7276}, + .time = Time(4.0209)}, + kTol), + ResultNear({.position = {-6.0081, -2.0009}, + .velocity = {-0.5170, -0.1628}, + .acceleration = {-5.9729, -24.2711}, + .time = Time(4.0251)}, + kTol), + ResultNear({.position = {-6.0102, -2.0021}, + .velocity = {-0.5079, -0.2868}, + .acceleration = {2.1807, -29.7000}, + .time = Time(4.0292)}, + kTol), + ResultNear({.position = {-6.0122, -2.0039}, + .velocity = {-0.4755, -0.4203}, + .acceleration = {7.7710, -31.9724}, + .time = Time(4.0334)}, + kTol))); + + time += kDeltaTime; + results.clear(); + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kMove, + .position = {-6.04, -2.02}, + .time = time}, + results) + .ok()); + EXPECT_THAT(results, + ElementsAre(ResultNear({.position = {-6.0143, -2.0059}, + .velocity = {-0.4899, -0.4921}, + .acceleration = {-3.4456, -17.1929}, + .time = Time(4.0376)}, + kTol), + ResultNear({.position = {-6.0165, -2.0081}, + .velocity = {-0.5363, -0.5170}, + .acceleration = {-11.1122, -5.9734}, + .time = Time(4.0418)}, + kTol), + ResultNear({.position = {-6.0190, -2.0102}, + .velocity = {-0.6027, -0.5079}, + .acceleration = {-15.9053, 2.1804}, + .time = Time(4.0459)}, + kTol), + ResultNear({.position = {-6.0218, -2.0122}, + .velocity = {-0.6796, -0.4755}, + .acceleration = {-18.4402, 7.7708}, + .time = Time(4.0501)}, + kTol))); + + time += kDeltaTime; + results.clear(); + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kMove, + .position = {-6.04, -2.04}, + .time = time}, + results) + .ok()); + EXPECT_THAT(results, + ElementsAre(ResultNear({.position = {-6.0248, -2.0143}, + .velocity = {-0.6986, -0.4899}, + .acceleration = {-4.5389, -3.4458}, + .time = Time(4.0543)}, + kTol), + ResultNear({.position = {-6.0276, -2.0165}, + .velocity = {-0.6760, -0.5363}, + .acceleration = {5.4168, -11.1130}, + .time = Time(4.0585)}, + kTol), + ResultNear({.position = {-6.0302, -2.0190}, + .velocity = {-0.6255, -0.6027}, + .acceleration = {12.1018, -15.9045}, + .time = Time(4.0626)}, + kTol), + ResultNear({.position = {-6.0325, -2.0218}, + .velocity = {-0.5580, -0.6796}, + .acceleration = {16.1550, -18.4404}, + .time = Time(4.0668)}, + kTol))); +} + +TEST(StrokeModelerWithNewProjectionTest, UpdateAppendsToResults) { + const Duration kDeltaTime{1. / 300}; + + StrokeModeler modeler; + ASSERT_TRUE(modeler.Reset(kDefaultParams).ok()); + + Time time{2}; + std::vector results; + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kDown, + .position = {5, -3}, + .time = time}, + results) + .ok()); + auto first_result_matcher = + ResultNear({.position = {5, -3}, .time = Time(2)}, kTol); + EXPECT_THAT(results, ElementsAre(first_result_matcher)); + + time += kDeltaTime; + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kMove, + .position = {5, -3.1}, + .time = time}, + results) + .ok()); + EXPECT_THAT(results, ElementsAre(first_result_matcher, + ResultNear({.position = {5, -3.0033}, + .velocity = {0, -0.9818}, + .acceleration = {0, -294.5452}, + .time = Time(2.0033)}, + kTol))); +} + +TEST(StrokeModelerWithNewProjectionTest, Reset) { + const Duration kDeltaTime{1. / 50}; + + StrokeModeler modeler; + ASSERT_TRUE(modeler.Reset(kDefaultParams).ok()); + + Time time{0}; + std::vector results; + + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kDown, + .position = {-8, -10}, + .time = time}, + results) + .ok()); + EXPECT_THAT(results, Not(IsEmpty())); + + ASSERT_TRUE(modeler.Predict(results).ok()); + EXPECT_THAT(results, IsEmpty()); + + time += kDeltaTime; + results.clear(); + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kMove, .time = time}, + results) + .ok()); + EXPECT_THAT(results, Not(IsEmpty())); + + ASSERT_TRUE(modeler.Predict(results).ok()); + EXPECT_THAT(results, Not(IsEmpty())); + + time += kDeltaTime; + results.clear(); + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kMove, + .position = {-11, -5}, + .time = time}, + results) + .ok()); + EXPECT_THAT(results, Not(IsEmpty())); + + ASSERT_TRUE(modeler.Predict(results).ok()); + EXPECT_THAT(results, Not(IsEmpty())); + + ASSERT_TRUE(modeler.Reset(kDefaultParams).ok()); + EXPECT_EQ(modeler.Predict(results).code(), + absl::StatusCode::kFailedPrecondition); +} + +TEST(StrokeModelerWithNewProjectionTest, IgnoreInputsBeforeTDown) { + StrokeModeler modeler; + ASSERT_TRUE(modeler.Reset(kDefaultParams).ok()); + + std::vector results; + EXPECT_EQ(modeler + .Update({.event_type = Input::EventType::kMove, + .position = {0, 0}, + .time = Time(0)}, + results) + .code(), + absl::StatusCode::kFailedPrecondition); + EXPECT_THAT(results, IsEmpty()); + + results.clear(); + EXPECT_EQ(modeler + .Update({.event_type = Input::EventType::kUp, + .position = {0, 0}, + .time = Time(1)}, + results) + .code(), + absl::StatusCode::kFailedPrecondition); + EXPECT_THAT(results, IsEmpty()); +} + +TEST(StrokeModelerWithNewProjectionTest, IgnoreTDownWhileStrokeIsInProgress) { + StrokeModeler modeler; + ASSERT_TRUE(modeler.Reset(kDefaultParams).ok()); + + std::vector results; + + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kDown, + .position = {0, 0}, + .time = Time(0)}, + results) + .ok()); + EXPECT_THAT(results, Not(IsEmpty())); + + results.clear(); + EXPECT_EQ(modeler + .Update({.event_type = Input::EventType::kDown, + .position = {1, 1}, + .time = Time(1)}, + results) + .code(), + absl::StatusCode::kFailedPrecondition); + EXPECT_THAT(results, IsEmpty()); + + results.clear(); + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kMove, + .position = {1, 1}, + .time = Time(1)}, + results) + .ok()); + EXPECT_THAT(results, Not(IsEmpty())); + + results.clear(); + EXPECT_EQ(modeler + .Update({.event_type = Input::EventType::kDown, + .position = {2, 2}, + .time = Time(2)}, + results) + .code(), + absl::StatusCode::kFailedPrecondition); + EXPECT_THAT(results, IsEmpty()); +} + +TEST(StrokeModelerWithNewProjectionTest, AlternateParams) { + const Duration kDeltaTime{1. / 50}; + + StrokeModelParams stroke_model_params = kDefaultParams; + stroke_model_params.sampling_params.min_output_rate = 70; + + StrokeModeler modeler; + ASSERT_TRUE(modeler.Reset(stroke_model_params).ok()); + + Time time{3}; + std::vector results; + + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kDown, + .position = {0, 0}, + .time = time, + .pressure = .5, + .tilt = .2, + .orientation = .4}, + results) + .ok()); + EXPECT_THAT(results, + ElementsAre(ResultNear( + {{0, 0}, {0, 0}, {0, 0}, Time{3}, .5, .2, .4}, kTol))); + + ASSERT_TRUE(modeler.Predict(results).ok()); + EXPECT_THAT(results, IsEmpty()); + + time += kDeltaTime; + results.clear(); + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kMove, + .position = {0, .5}, + .time = time, + .pressure = .4, + .tilt = .3, + .orientation = .3}, + results) + .ok()); + EXPECT_THAT(results, ElementsAre(ResultNear({.position = {0, 0.0736}, + .velocity = {0, 7.3636}, + .acceleration = {0, 736.3636}, + .time = Time(3.0100), + .pressure = 0.5, + .tilt = 0.2, + .orientation = 0.4}, + kTol), + ResultNear({.position = {0, 0.2198}, + .velocity = {0, 14.6202}, + .acceleration = {0, 725.6529}, + .time = Time(3.0200), + .pressure = 0.4560, + .tilt = 0.2440, + .orientation = 0.3560}, + kTol))); + + ASSERT_TRUE(modeler.Predict(results).ok()); + EXPECT_THAT(results, ElementsAre(ResultNear({.position = {0, 0.3823}, + .velocity = {0, 11.3709}, + .acceleration = {0, -227.4474}, + .time = Time(3.0343), + .pressure = 0.4235, + .tilt = 0.2765, + .orientation = 0.3235}, + kTol), + ResultNear({.position = {0, 0.4484}, + .velocity = {0, 4.6285}, + .acceleration = {0, -471.9660}, + .time = Time(3.0486), + .pressure = 0.4103, + .tilt = 0.2897, + .orientation = 0.3103}, + kTol), + ResultNear({.position = {0, 0.4775}, + .velocity = {0, 2.0389}, + .acceleration = {0, -181.2747}, + .time = Time(3.0629), + .pressure = 0.4045, + .tilt = 0.2955, + .orientation = 0.3045}, + kTol), + ResultNear({.position = {0, 0.4902}, + .velocity = {0, 0.8873}, + .acceleration = {0, -80.6136}, + .time = Time(3.0771), + .pressure = 0.4020, + .tilt = 0.2980, + .orientation = 0.3020}, + kTol), + ResultNear({.position = {0, 0.4957}, + .velocity = {0, 0.3868}, + .acceleration = {0, -35.0318}, + .time = Time(3.0914), + .pressure = 0.4009, + .tilt = 0.2991, + .orientation = 0.3009}, + kTol), + ResultNear({.position = {0, 0.4981}, + .velocity = {0, 0.1686}, + .acceleration = {0, -15.2760}, + .time = Time(3.1057), + .pressure = 0.4004, + .tilt = 0.2996, + .orientation = 0.3004}, + kTol), + ResultNear({.position = {0, 0.4992}, + .velocity = {0, 0.0735}, + .acceleration = {0, -6.6579}, + .time = Time(3.1200), + .pressure = 0.4002, + .tilt = 0.2998, + .orientation = 0.3002}, + kTol))); + + time += kDeltaTime; + results.clear(); + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kMove, + .position = {.2, 1}, + .time = time, + .pressure = .3, + .tilt = .4, + .orientation = .2}, + results) + .ok()); + EXPECT_THAT(results, + ElementsAre(ResultNear({.position = {0.0295, 0.4169}, + .velocity = {2.9455, 19.7093}, + .acceleration = {294.5455, 508.9161}, + .time = Time(3.0300), + .pressure = 0.4162, + .tilt = 0.2838, + .orientation = 0.3162}, + kTol), + ResultNear({.position = {0.0879, 0.6439}, + .velocity = {5.8481, 22.6926}, + .acceleration = {290.2612, 298.3311}, + .time = Time(3.0400), + .pressure = 0.3696, + .tilt = 0.3304, + .orientation = 0.2696}, + kTol))); + + ASSERT_TRUE(modeler.Predict(results).ok()); + EXPECT_THAT(results, + ElementsAre(ResultNear({.position = {0.1529, 0.8487}, + .velocity = {4.5484, 14.3374}, + .acceleration = {-90.9790, -584.8687}, + .time = Time(3.0543), + .pressure = 0.3293, + .tilt = 0.3707, + .orientation = 0.2293}, + kTol), + ResultNear({.position = {0.1794, 0.9338}, + .velocity = {1.8514, 5.9577}, + .acceleration = {-188.7864, -586.5760}, + .time = Time(3.0686), + .pressure = 0.4220, + .tilt = 0.2780, + .orientation = 0.3220}, + kTol), + ResultNear({.position = {0.1910, 0.9712}, + .velocity = {0.8156, 2.6159}, + .acceleration = {-72.5099, -233.9289}, + .time = Time(3.0829), + .pressure = 0.3003, + .tilt = 0.3997, + .orientation = 0.2003}, + kTol), + ResultNear({.position = {0.1961, 0.9874}, + .velocity = {0.3549, 1.1389}, + .acceleration = {-32.2455, -103.3868}, + .time = Time(3.0971), + .pressure = 0.4281, + .tilt = 0.2719, + .orientation = 0.3281}, + kTol), + ResultNear({.position = {0.1983, 0.9945}, + .velocity = {0.1547, 0.4965}, + .acceleration = {-14.0127, -44.9693}, + .time = Time(3.1114), + .pressure = 0.3000, + .tilt = 0.4000, + .orientation = 0.2000}, + kTol), + ResultNear({.position = {0.1993, 0.9976}, + .velocity = {0.0674, 0.2164}, + .acceleration = {-6.1104, -19.6068}, + .time = Time(3.1257), + .pressure = 0.4282, + .tilt = 0.2718, + .orientation = 0.3282}, + kTol), + ResultNear({.position = {0.1997, 0.9990}, + .velocity = {0.0294, 0.0943}, + .acceleration = {-2.6631, -8.5455}, + .time = Time(3.1400), + .pressure = 0.3000, + .tilt = 0.4000, + .orientation = 0.2000}, + kTol))); + + time += kDeltaTime; + results.clear(); + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kMove, + .position = {.4, 1.4}, + .time = time, + .pressure = .2, + .tilt = .7, + .orientation = 0}, + results) + .ok()); + EXPECT_THAT(results, + ElementsAre(ResultNear({.position = {0.1668, 0.8712}, + .velocity = {7.8837, 22.7349}, + .acceleration = {203.5665, 4.2224}, + .time = Time(3.0500), + .pressure = 0.3248, + .tilt = 0.3752, + .orientation = 0.2248}, + kTol), + ResultNear({.position = {0.2575, 1.0906}, + .velocity = {9.0771, 21.9411}, + .acceleration = {119.3324, -79.3721}, + .time = Time(3.0600), + .pressure = 0.2762, + .tilt = 0.4713, + .orientation = 0.1524}, + kTol))); + + ASSERT_TRUE(modeler.Predict(results).ok()); + EXPECT_THAT(results, + ElementsAre(ResultNear({.position = {0.3395, 1.2676}, + .velocity = {5.7349, 12.3913}, + .acceleration = {-233.9475, -668.4906}, + .time = Time(3.0743), + .pressure = 0.2325, + .tilt = 0.6028, + .orientation = 0.0648}, + kTol), + ResultNear({.position = {0.3735, 1.3421}, + .velocity = {2.3831, 5.2156}, + .acceleration = {-234.6304, -502.2992}, + .time = Time(3.0886), + .pressure = 0.3287, + .tilt = 0.3713, + .orientation = 0.2287}, + kTol), + ResultNear({.position = {0.3885, 1.3748}, + .velocity = {1.0463, 2.2854}, + .acceleration = {-93.5716, -205.1091}, + .time = Time(3.1029), + .pressure = 0.2005, + .tilt = 0.6985, + .orientation = 0.0010}, + kTol), + ResultNear({.position = {0.3950, 1.3890}, + .velocity = {0.4556, 0.9954}, + .acceleration = {-41.3547, -90.3064}, + .time = Time(3.1171), + .pressure = 0.3573, + .tilt = 0.3427, + .orientation = 0.2573}, + kTol), + ResultNear({.position = {0.3978, 1.3952}, + .velocity = {0.1986, 0.4339}, + .acceleration = {-17.9877, -39.3021}, + .time = Time(3.1314), + .pressure = 0.3586, + .tilt = 0.3414, + .orientation = 0.2586}, + kTol), + ResultNear({.position = {0.3990, 1.3979}, + .velocity = {0.0866, 0.1891}, + .acceleration = {-7.8428, -17.1346}, + .time = Time(3.1457), + .pressure = 0.2005, + .tilt = 0.6985, + .orientation = 0.0010}, + kTol), + ResultNear({.position = {0.3996, 1.3991}, + .velocity = {0.0377, 0.0824}, + .acceleration = {-3.4182, -7.4680}, + .time = Time(3.1600), + .pressure = 0.3579, + .tilt = 0.3421, + .orientation = 0.2579}, + kTol))); + + time += kDeltaTime; + results.clear(); + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kUp, + .position = {.7, 1.7}, + .time = time, + .pressure = .1, + .tilt = 1, + .orientation = 0}, + results) + .ok()); + EXPECT_THAT(results, + ElementsAre(ResultNear({.position = {0.3691, 1.2874}, + .velocity = {11.1558, 19.6744}, + .acceleration = {207.8707, -226.6725}, + .time = Time(3.0700), + .pressure = 0.2250, + .tilt = 0.6250, + .orientation = 0.0500}, + kTol), + ResultNear({.position = {0.4978, 1.4640}, + .velocity = {12.8701, 17.6629}, + .acceleration = {171.4340, -201.1508}, + .time = Time(3.0800), + .pressure = 0.1736, + .tilt = 0.7793, + .orientation = 0}, + kTol), + ResultNear({.position = {0.6141, 1.5986}, + .velocity = {8.1404, 9.4261}, + .acceleration = {-331.0815, -576.5752}, + .time = Time(3.0943), + .pressure = 0.1308, + .tilt = 0.9078, + .orientation = 0}, + kTol), + ResultNear({.position = {0.6624, 1.6557}, + .velocity = {3.3822, 3.9953}, + .acceleration = {-333.0701, -380.1579}, + .time = Time(3.1086), + .pressure = 0.2200, + .tilt = 0.6401, + .orientation = 0.0400}, + kTol), + ResultNear({.position = {0.6836, 1.6807}, + .velocity = {1.4851, 1.7488}, + .acceleration = {-132.8005, -157.2520}, + .time = Time(3.1229), + .pressure = 0.1004, + .tilt = 0.9988, + .orientation = 0}, + kTol), + ResultNear({.position = {0.6929, 1.6916}, + .velocity = {0.6466, 0.7618}, + .acceleration = {-58.6943, -69.0946}, + .time = Time(3.1371), + .pressure = 0.2321, + .tilt = 0.6038, + .orientation = 0.0641}, + kTol), + ResultNear({.position = {0.6969, 1.6963}, + .velocity = {0.2819, 0.3321}, + .acceleration = {-25.5298, -30.0794}, + .time = Time(3.1514), + .pressure = 0.2324, + .tilt = 0.6026, + .orientation = 0.0650}, + kTol), + ResultNear({.position = {0.6986, 1.6984}, + .velocity = {0.1229, 0.1447}, + .acceleration = {-11.1311, -13.1133}, + .time = Time(3.1657), + .pressure = 0.1005, + .tilt = 0.9985, + .orientation = 0}, + kTol), + ResultNear({.position = {0.6994, 1.6993}, + .velocity = {0.0535, 0.0631}, + .acceleration = {-4.8514, -5.7153}, + .time = Time(3.1800), + .pressure = 0.1000, + .tilt = 0.9999, + .orientation = 0}, + kTol))); + + EXPECT_EQ(modeler.Predict(results).code(), + absl::StatusCode::kFailedPrecondition); +} + +TEST(StrokeModelerWithNewProjectionTest, GenerateOutputOnTUpEvenIfNoTimeDelta) { + const Duration kDeltaTime{1. / 500}; + + StrokeModeler modeler; + ASSERT_TRUE(modeler.Reset(kDefaultParams).ok()); + + Time time{0}; + std::vector results; + results.clear(); + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kDown, + .position = {5, 5}, + .time = time}, + results) + .ok()); + EXPECT_THAT(results, ElementsAre(ResultNear( + {.position = {5, 5}, .time = Time(0)}, kTol))); + + time += kDeltaTime; + results.clear(); + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kMove, + .position = {5, 5}, + .time = time}, + results) + .ok()); + EXPECT_THAT(results, ElementsAre(ResultNear( + {.position = {5, 5}, .time = Time(0.002)}, kTol))); + + results.clear(); + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kUp, + .position = {5, 5}, + .time = time}, + results) + .ok()); + EXPECT_THAT(results, ElementsAre(ResultNear( + {.position = {5, 5}, .time = Time(0.0076)}, kTol))); +} + +TEST(StrokeModelerWithNewProjectionTest, RejectInputIfNegativeTimeDelta) { + StrokeModeler modeler; + ASSERT_TRUE(modeler.Reset(kDefaultParams).ok()); + + std::vector results; + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kDown, + .position = {0, 0}, + .time = Time(0)}, + results) + .ok()); + EXPECT_THAT(results, Not(IsEmpty())); + + results.clear(); + EXPECT_EQ(modeler + .Update({.event_type = Input::EventType::kMove, + .position = {1, 1}, + .time = Time(-.1)}, + results) + .code(), + absl::StatusCode::kInvalidArgument); + EXPECT_THAT(results, IsEmpty()); + + results.clear(); + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kMove, + .position = {1, 1}, + .time = Time(1)}, + results) + .ok()); + EXPECT_THAT(results, Not(IsEmpty())); + + results.clear(); + EXPECT_EQ(modeler + .Update({.event_type = Input::EventType::kUp, + .position = {1, 1}, + .time = Time(.9)}, + results) + .code(), + absl::StatusCode::kInvalidArgument); + EXPECT_THAT(results, IsEmpty()); +} + +TEST(StrokeModelerWithNewProjectionTest, RejectDuplicateInput) { + StrokeModeler modeler; + ASSERT_TRUE(modeler.Reset(kDefaultParams).ok()); + + std::vector results; + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kDown, + .position = {0, 0}, + .time = Time(0), + .pressure = .2, + .tilt = .3, + .orientation = .4}, + results) + .ok()); + EXPECT_THAT(results, Not(IsEmpty())); + + results.clear(); + EXPECT_EQ(modeler + .Update({.event_type = Input::EventType::kDown, + .position = {0, 0}, + .time = Time(0), + .pressure = .2, + .tilt = .3, + .orientation = .4}, + results) + .code(), + absl::StatusCode::kInvalidArgument); + EXPECT_THAT(results, IsEmpty()); + + results.clear(); + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kMove, + .position = {1, 2}, + .time = Time(1), + .pressure = .1, + .tilt = .2, + .orientation = .3}, + results) + .ok()); + EXPECT_THAT(results, Not(IsEmpty())); + + results.clear(); + EXPECT_EQ(modeler + .Update({.event_type = Input::EventType::kMove, + .position = {1, 2}, + .time = Time(1), + .pressure = .1, + .tilt = .2, + .orientation = .3}, + results) + .code(), + absl::StatusCode::kInvalidArgument); + EXPECT_THAT(results, IsEmpty()); +} + +TEST(StrokeModelerWithNewProjectionTest, FarApartTimesDoNotCrashForMove) { + StrokeModeler modeler; + ASSERT_TRUE(modeler.Reset(kDefaultParams).ok()); + + std::vector results; + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kDown, + .position = {0, 0}, + .time = Time(0), + .pressure = .2, + .tilt = .3, + .orientation = .4}, + results) + .ok()); + EXPECT_THAT(results, Not(IsEmpty())); + + results.clear(); + EXPECT_EQ(modeler + .Update({.event_type = Input::EventType::kMove, + .position = {0, 0}, + .time = Time(INT_MAX), + .pressure = .2, + .tilt = .3, + .orientation = .4}, + results) + .code(), + absl::StatusCode::kInvalidArgument); + EXPECT_THAT(results, IsEmpty()); +} + +TEST(StrokeModelerWithNewProjectionTest, FarApartTimesDoNotCrashForUp) { + StrokeModeler modeler; + ASSERT_TRUE(modeler.Reset(kDefaultParams).ok()); + + std::vector results; + ASSERT_TRUE(modeler + .Update({.event_type = Input::EventType::kDown, + .position = {0, 0}, + .time = Time(0), + .pressure = .2, + .tilt = .3, + .orientation = .4}, + results) + .ok()); + EXPECT_THAT(results, Not(IsEmpty())); + + results.clear(); + EXPECT_EQ(modeler + .Update({.event_type = Input::EventType::kUp, + .position = {0, 0}, + .time = Time(INT_MAX), + .pressure = .2, + .tilt = .3, + .orientation = .4}, + results) + .code(), + absl::StatusCode::kInvalidArgument); + EXPECT_THAT(results, IsEmpty()); +} + +TEST(StrokeModelerWithNewProjectionTest, FirstResetMustPassParams) { + StrokeModeler modeler; + EXPECT_EQ(modeler.Reset().code(), absl::StatusCode::kFailedPrecondition); +} + +TEST(StrokeModelerWithNewProjectionTest, ResetKeepsParamsAndResetsStroke) { + StrokeModeler modeler; + // Initialize with parameters and update. + Input pointer_down{.event_type = Input::EventType::kDown, + .position = {3, 4}, + .time = Time(0)}; + ASSERT_TRUE(modeler.Reset(kDefaultParams).ok()); + std::vector results; + ASSERT_TRUE(modeler.Update(pointer_down, results).ok()); + + // Reset, using the same parameters. + ASSERT_TRUE(modeler.Reset().ok()); + + // Doesn't object to seeing a duplicate input or another down event, since + // the previous stroke in progress was aborted by the call to reset. + results.clear(); + ASSERT_TRUE(modeler.Update(pointer_down, results).ok()); +} + +TEST(StrokeModelerWithNewProjectionTest, SaveAndRestore) { + StrokeModeler modeler; + ASSERT_TRUE(modeler.Reset(kDefaultParams).ok()); + + std::vector results; + + // Create a save that will be overwritten. + modeler.Save(); + + EXPECT_TRUE(modeler + .Update({.event_type = Input::EventType::kDown, + .position = {-6, -2}, + .time = Time(4)}, + results) + .ok()); + EXPECT_THAT(results, ElementsAre(ResultNear( + {.position = {-6, -2}, .time = Time(4)}, kTol))); + + // Save a second time and then finish the stroke. + modeler.Save(); + + results.clear(); + EXPECT_TRUE(modeler + .Update({.event_type = Input::EventType::kUp, + .position = {-6.02, -2}, + .time = Time(4.0167)}, + results) + .ok()); + EXPECT_THAT(results, ElementsAre(ResultNear({.position = {-6.0003, -2}, + .velocity = {-0.0615, 0}, + .acceleration = {-14.7276, 0}, + .time = Time(4.0042)}, + kTol), + ResultNear({.position = {-6.0009, -2}, + .velocity = {-0.1628, 0}, + .acceleration = {-24.2725, 0}, + .time = Time(4.0084)}, + kTol), + ResultNear({.position = {-6.0021, -2}, + .velocity = {-0.2868, 0}, + .acceleration = {-29.6996, 0}, + .time = Time(4.0125)}, + kTol), + ResultNear({.position = {-6.0039, -2}, + .velocity = {-0.4203, 0}, + .acceleration = {-31.9728, 0}, + .time = Time(4.0167)}, + kTol), + ResultNear({.position = {-6.0068, -2}, + .velocity = {-0.5158, 0}, + .acceleration = {-17.1932, 0}, + .time = Time(4.0223)}, + kTol), + ResultNear({.position = {-6.0097, -2}, + .velocity = {-0.5262, 0}, + .acceleration = {-1.8749, 0}, + .time = Time(4.0278)}, + kTol), + ResultNear({.position = {-6.0124, -2}, + .velocity = {-0.4847, 0}, + .acceleration = {7.4861, 0}, + .time = Time(4.0334)}, + kTol), + ResultNear({.position = {-6.0147, -2}, + .velocity = {-0.4156, 0}, + .acceleration = {12.4229, 0}, + .time = Time(4.0389)}, + kTol), + ResultNear({.position = {-6.0165, -2}, + .velocity = {-0.3364, 0}, + .acceleration = {14.2557, 0}, + .time = Time(4.0445)}, + kTol), + ResultNear({.position = {-6.0180, -2}, + .velocity = {-0.2583, 0}, + .acceleration = {14.0591, 0}, + .time = Time(4.0500)}, + kTol), + ResultNear({.position = {-6.0190, -2}, + .velocity = {-0.1880, 0}, + .acceleration = {12.6630, 0}, + .time = Time(4.0556)}, + kTol))); + + // Restore and finish the stroke again. + modeler.Restore(); + results.clear(); + EXPECT_TRUE(modeler + .Update({.event_type = Input::EventType::kUp, + .position = {-6.02, -2}, + .time = Time(4.0167)}, + results) + .ok()); + EXPECT_THAT(results, ElementsAre(ResultNear({.position = {-6.0003, -2}, + .velocity = {-0.0615, 0}, + .acceleration = {-14.7276, 0}, + .time = Time(4.0042)}, + kTol), + ResultNear({.position = {-6.0009, -2}, + .velocity = {-0.1628, 0}, + .acceleration = {-24.2725, 0}, + .time = Time(4.0084)}, + kTol), + ResultNear({.position = {-6.0021, -2}, + .velocity = {-0.2868, 0}, + .acceleration = {-29.6996, 0}, + .time = Time(4.0125)}, + kTol), + ResultNear({.position = {-6.0039, -2}, + .velocity = {-0.4203, 0}, + .acceleration = {-31.9728, 0}, + .time = Time(4.0167)}, + kTol), + ResultNear({.position = {-6.0068, -2}, + .velocity = {-0.5158, 0}, + .acceleration = {-17.1932, 0}, + .time = Time(4.0223)}, + kTol), + ResultNear({.position = {-6.0097, -2}, + .velocity = {-0.5262, 0}, + .acceleration = {-1.8749, 0}, + .time = Time(4.0278)}, + kTol), + ResultNear({.position = {-6.0124, -2}, + .velocity = {-0.4847, 0}, + .acceleration = {7.4861, 0}, + .time = Time(4.0334)}, + kTol), + ResultNear({.position = {-6.0147, -2}, + .velocity = {-0.4156, 0}, + .acceleration = {12.4229, 0}, + .time = Time(4.0389)}, + kTol), + ResultNear({.position = {-6.0165, -2}, + .velocity = {-0.3364, 0}, + .acceleration = {14.2557, 0}, + .time = Time(4.0445)}, + kTol), + ResultNear({.position = {-6.0180, -2}, + .velocity = {-0.2583, 0}, + .acceleration = {14.0591, 0}, + .time = Time(4.0500)}, + kTol), + ResultNear({.position = {-6.0190, -2}, + .velocity = {-0.1880, 0}, + .acceleration = {12.6630, 0}, + .time = Time(4.0556)}, + kTol))); + + // Restoring should not have cleared the save, so repeat one more time. + modeler.Restore(); + results.clear(); + EXPECT_TRUE(modeler + .Update({.event_type = Input::EventType::kUp, + .position = {-6.02, -2}, + .time = Time{4.0167}}, + results) + .ok()); + EXPECT_THAT(results, ElementsAre(ResultNear({.position = {-6.0003, -2}, + .velocity = {-0.0615, 0}, + .acceleration = {-14.7276, 0}, + .time = Time(4.0042)}, + kTol), + ResultNear({.position = {-6.0009, -2}, + .velocity = {-0.1628, 0}, + .acceleration = {-24.2725, 0}, + .time = Time(4.0084)}, + kTol), + ResultNear({.position = {-6.0021, -2}, + .velocity = {-0.2868, 0}, + .acceleration = {-29.6996, 0}, + .time = Time(4.0125)}, + kTol), + ResultNear({.position = {-6.0039, -2}, + .velocity = {-0.4203, 0}, + .acceleration = {-31.9728, 0}, + .time = Time(4.0167)}, + kTol), + ResultNear({.position = {-6.0068, -2}, + .velocity = {-0.5158, 0}, + .acceleration = {-17.1932, 0}, + .time = Time(4.0223)}, + kTol), + ResultNear({.position = {-6.0097, -2}, + .velocity = {-0.5262, 0}, + .acceleration = {-1.8749, 0}, + .time = Time(4.0278)}, + kTol), + ResultNear({.position = {-6.0124, -2}, + .velocity = {-0.4847, 0}, + .acceleration = {7.4861, 0}, + .time = Time(4.0334)}, + kTol), + ResultNear({.position = {-6.0147, -2}, + .velocity = {-0.4156, 0}, + .acceleration = {12.4229, 0}, + .time = Time(4.0389)}, + kTol), + ResultNear({.position = {-6.0165, -2}, + .velocity = {-0.3364, 0}, + .acceleration = {14.2557, 0}, + .time = Time(4.0445)}, + kTol), + ResultNear({.position = {-6.0180, -2}, + .velocity = {-0.2583, 0}, + .acceleration = {14.0591, 0}, + .time = Time(4.0500)}, + kTol), + ResultNear({.position = {-6.0190, -2}, + .velocity = {-0.1880, 0}, + .acceleration = {12.6630, 0}, + .time = Time(4.0556)}, + kTol))); + + // Calling Reset() should clear the save point so calling Restore() again + // should have no effect. + EXPECT_TRUE(modeler.Reset().ok()); + results.clear(); + EXPECT_TRUE(modeler + .Update({.event_type = Input::EventType::kDown, + .position = {-6, -2}, + .time = Time(4)}, + results) + .ok()); + EXPECT_THAT(results, ElementsAre(ResultNear( + {.position = {-6, -2}, .time = Time(4)}, kTol))); + results.clear(); + EXPECT_TRUE(modeler + .Update({.event_type = Input::EventType::kUp, + .position = {-6.02, -2}, + .time = Time(4.0167)}, + results) + .ok()); + + modeler.Restore(); + + // Restore should have no effect so we cannot finish the line again. + results.clear(); + EXPECT_EQ(modeler + .Update({.event_type = Input::EventType::kUp, + .position = {-6.02, -2}, + .time = Time(4.0167)}, + results) + .code(), + absl::StatusCode::kFailedPrecondition); +} + +TEST(StrokeModelerWithNewProjectionTest, + UpdateToUpdateIdenticalToUpdatePredictUpdate) { + Input input1 = {.event_type = Input::EventType::kDown, + .position = {-6, -2}, + .time = Time(0), + .pressure = 0.5, + .tilt = 0.5, + .orientation = 0.5}; + + Input input2 = {.event_type = Input::EventType::kMove, + .position = {-6.2, -2}, + .time = Time(.0167), + .pressure = 0.48, + .tilt = 0.4, + .orientation = 0.4}; + Input input3 = {.event_type = Input::EventType::kMove, + .position = {-6.2, -2.2}, + .time = Time(0.0334), + .pressure = 0.48, + .tilt = 0.4, + .orientation = 0.4}; + + StrokeModeler modeler; + StrokeModelParams params = kDefaultParams; + params.position_modeler_params.loop_contraction_mitigation_params = + PositionModelerParams::LoopContractionMitigationParameters{ + .is_enabled = true, + .speed_lower_bound = 0, + .speed_upper_bound = 80, + .interpolation_strength_at_speed_lower_bound = 0.7, + .interpolation_strength_at_speed_upper_bound = 0, + .n_speed_samples = 10}; + params.stylus_state_modeler_params + .project_to_segment_along_normal_is_enabled = true; + ASSERT_TRUE(modeler.Reset(params).ok()); + + std::vector results_with_predict; + std::vector results_without_predict; + + // Run the first 2 input updates, then a prediction then the last input. + ASSERT_TRUE(modeler.Update(input1, results_with_predict).ok()); + results_with_predict.clear(); + ASSERT_TRUE(modeler.Update(input2, results_with_predict).ok()); + results_with_predict.clear(); + ASSERT_TRUE(modeler.Predict(results_with_predict).ok()); + results_with_predict.clear(); + ASSERT_TRUE(modeler.Update(input3, results_with_predict).ok()); + + // Reset the modeler and run the same 3 input updates again but without + // prediction. + ASSERT_TRUE(modeler.Reset(params).ok()); + ASSERT_TRUE(modeler.Update(input1, results_without_predict).ok()); + results_without_predict.clear(); + ASSERT_TRUE(modeler.Update(input2, results_without_predict).ok()); + results_without_predict.clear(); + ASSERT_TRUE(modeler.Update(input3, results_without_predict).ok()); + + // The results for both runs should be the same. + EXPECT_EQ(results_with_predict, results_without_predict); +} + +} // namespace +} // namespace stroke_model +} // namespace ink diff --git a/ink_stroke_modeler/type_matchers.cc b/ink_stroke_modeler/type_matchers.cc new file mode 100644 index 0000000..a290b9f --- /dev/null +++ b/ink_stroke_modeler/type_matchers.cc @@ -0,0 +1,47 @@ +#include "ink_stroke_modeler/type_matchers.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "absl/strings/str_cat.h" +#include "ink_stroke_modeler/internal/type_matchers.h" +#include "ink_stroke_modeler/types.h" + +namespace ink { +namespace stroke_model { +namespace { + +using ::testing::AllOf; +using ::testing::ExplainMatchResult; +using ::testing::Field; +using ::testing::FloatNear; +using ::testing::Matcher; +using ::testing::PrintToString; + +MATCHER_P2(ResultNearMatcher, expected, tolerance, + absl::StrCat(negation ? "doesn't approximately match" + : "approximately matches", + " Result (expected: ", PrintToString(expected), + ", tolerance: ", PrintToString(tolerance), ")")) { + return ExplainMatchResult( + AllOf(Field("position", &Result::position, + Vec2Near(expected.position, tolerance)), + Field("velocity", &Result::velocity, + Vec2Near(expected.velocity, tolerance)), + Field("acceleration", &Result::acceleration, + Vec2Near(expected.acceleration, tolerance)), + Field("time", &Result::time, TimeNear(expected.time, tolerance)), + Field("pressure", &Result::pressure, + FloatNear(expected.pressure, tolerance)), + Field("tilt", &Result::tilt, FloatNear(expected.tilt, tolerance)), + Field("orientation", &Result::orientation, + FloatNear(expected.orientation, tolerance))), + arg, result_listener); +} + +} // namespace +Matcher ResultNear(const Result &expected, float tolerance) { + return ResultNearMatcher(expected, tolerance); +} + +} // namespace stroke_model +} // namespace ink diff --git a/ink_stroke_modeler/type_matchers.h b/ink_stroke_modeler/type_matchers.h new file mode 100644 index 0000000..2a0d312 --- /dev/null +++ b/ink_stroke_modeler/type_matchers.h @@ -0,0 +1,15 @@ +#ifndef INK_STROKE_MODELER_TYPE_MATCHERS_H_ +#define INK_STROKE_MODELER_TYPE_MATCHERS_H_ + +#include "gtest/gtest.h" +#include "ink_stroke_modeler/types.h" + +namespace ink { +namespace stroke_model { + +::testing::Matcher ResultNear(const Result &expected, float tolerance); + +} // namespace stroke_model +} // namespace ink + +#endif // INK_STROKE_MODELER_TYPE_MATCHERS_H_ diff --git a/ink_stroke_modeler/types.h b/ink_stroke_modeler/types.h index 668e7a6..d831260 100644 --- a/ink_stroke_modeler/types.h +++ b/ink_stroke_modeler/types.h @@ -213,6 +213,9 @@ struct Result { float orientation = -1; }; +bool operator==(const Result &lhs, const Result &rhs); +bool operator!=(const Result &lhs, const Result &rhs); + std::string ToFormattedString(const Result &result); template @@ -376,6 +379,16 @@ inline bool operator!=(const Input &lhs, const Input &rhs) { return !(lhs == rhs); } +inline bool operator==(const Result &lhs, const Result &rhs) { + return lhs.position == rhs.position && lhs.velocity == rhs.velocity && + lhs.acceleration == rhs.acceleration && lhs.time == rhs.time && + lhs.pressure == rhs.pressure && lhs.tilt == rhs.tilt && + lhs.orientation == rhs.orientation; +} +inline bool operator!=(const Result &lhs, const Result &rhs) { + return !(lhs == rhs); +} + inline std::ostream &operator<<(std::ostream &s, Input::EventType event_type) { return s << ToFormattedString(event_type); }