diff --git a/ink_stroke_modeler/internal/BUILD.bazel b/ink_stroke_modeler/internal/BUILD.bazel index f35a601..6811ff0 100644 --- a/ink_stroke_modeler/internal/BUILD.bazel +++ b/ink_stroke_modeler/internal/BUILD.bazel @@ -152,6 +152,7 @@ cc_library( ":utils", "//ink_stroke_modeler:params", "//ink_stroke_modeler:types", + "@com_google_absl//absl/log", ], ) diff --git a/ink_stroke_modeler/internal/stylus_state_modeler.cc b/ink_stroke_modeler/internal/stylus_state_modeler.cc index 48420bd..df51c4f 100644 --- a/ink_stroke_modeler/internal/stylus_state_modeler.cc +++ b/ink_stroke_modeler/internal/stylus_state_modeler.cc @@ -15,6 +15,7 @@ #include "ink_stroke_modeler/internal/stylus_state_modeler.h" #include +#include #include #include @@ -25,6 +26,27 @@ namespace ink { namespace stroke_model { +namespace { + +bool ShouldDropOldestInput( + const StylusStateModelerParams ¶ms, + const std::deque &raw_input_and_stylus_states) { + if (params.use_stroke_normal_projection) { + return raw_input_and_stylus_states.size() > params.min_input_samples && + // Check the difference between the newest and second-oldest inputs + // -- if that's greater than `min_sample_duration` then we can drop + // the oldest without going below `min_sample_duration`. + // Since `min_input_samples` > 0, the clause above guarantees that we + // have at least two inputs. + (raw_input_and_stylus_states.back().time - + raw_input_and_stylus_states[1].time) > params.min_sample_duration; + + } else { + return raw_input_and_stylus_states.size() > params.max_input_samples; + } +} + +} // namespace void StylusStateModeler::Update(Vec2 position, Time time, const StylusState &state) { @@ -69,9 +91,7 @@ void StylusStateModeler::Update(Vec2 position, Time time, .orientation = state.orientation, }); - if (params_.max_input_samples < 0 || - state_.raw_input_and_stylus_states.size() > - static_cast(params_.max_input_samples)) { + while (ShouldDropOldestInput(params_, state_.raw_input_and_stylus_states)) { state_.raw_input_and_stylus_states.pop_front(); } } @@ -85,7 +105,104 @@ void StylusStateModeler::Reset(const StylusStateModelerParams ¶ms) { params_ = params; } -Result StylusStateModeler::Query(Vec2 position, +namespace { + +// The location of the projection point along the raw input polyline. +struct RawInputProjection { + int segment_index; + float ratio_along_segment; +}; + +std::optional ProjectAlongStrokeNormal( + Vec2 position, Vec2 acceleration, Time time, Vec2 stroke_normal, + const std::deque &raw_input_polyline) { + // We track the best candidate separately for the left and right sides of the + // stroke, in case the closest projection is not in the right direction. + std::optional best_left_projection; + std::optional best_right_projection; + float best_distance_left = std::numeric_limits::infinity(); + float best_distance_right = std::numeric_limits::infinity(); + + // Update `best_projection` and `best_distance` if needed. + auto maybe_update_projection = + [](RawInputProjection candidate, float distance, + std::optional &best_projection, + float &best_distance) { + if (distance < best_distance) { + best_projection = candidate; + best_distance = distance; + } + }; + + for (decltype(raw_input_polyline.size()) i = 0; + i < raw_input_polyline.size() - 1; ++i) { + const Vec2 segment_start = raw_input_polyline[i].position; + const Vec2 segment_end = raw_input_polyline[i + 1].position; + + // Find the intersection of the stroke normal with the polyline segment. + std::optional segment_ratio = ProjectToSegmentAlongNormal( + segment_start, segment_end, position, stroke_normal); + if (!segment_ratio.has_value()) continue; + + Vec2 projection = Interp(segment_start, segment_end, *segment_ratio); + float distance = Distance(position, projection); + + // We update either the best left or the right projection, depending which + // side of the stroke it lies on -- recall that the stroke normal always + // points to the left. + RawInputProjection candidate{.segment_index = static_cast(i), + .ratio_along_segment = *segment_ratio}; + if (Vec2::DotProduct(projection - position, stroke_normal) < 0) { + maybe_update_projection(candidate, distance, best_right_projection, + best_distance_right); + } else { + maybe_update_projection(candidate, distance, best_left_projection, + best_distance_left); + } + } + + if (best_left_projection.has_value() && best_right_projection.has_value()) { + // We have candidate projections on both sides of the stroke, so we want to + // choose the one on the "outside" of the turn. The acceleration will always + // point to the "inside" of the curve, so we can compare it to the stroke + // normal (which always points left) to determine whether to use the left or + // right candidate. + return Vec2::DotProduct(stroke_normal, acceleration) > 0 + ? best_right_projection + : best_left_projection; + } + + // We have at most one projection -- return it if we have it. If we have + // neither, this returns std::nullopt, which is exactly what we want. + return best_right_projection.has_value() ? best_right_projection + : best_left_projection; +} + +std::optional ProjectToClosestPoint( + Vec2 position, const std::deque &raw_input_polyline) { + std::optional best_projection; + float min_distance = std::numeric_limits::infinity(); + for (decltype(raw_input_polyline.size()) i = 0; + i < raw_input_polyline.size() - 1; ++i) { + const Vec2 segment_start = raw_input_polyline[i].position; + const Vec2 segment_end = raw_input_polyline[i + 1].position; + float segment_ratio = + NearestPointOnSegment(segment_start, segment_end, position); + float distance = + Distance(position, Interp(segment_start, segment_end, segment_ratio)); + if (distance <= min_distance) { + best_projection = + RawInputProjection{.segment_index = static_cast(i), + .ratio_along_segment = segment_ratio}; + min_distance = distance; + } + } + return best_projection; +} + +} // namespace + +Result StylusStateModeler::Query(Vec2 position, Vec2 acceleration, std::optional stroke_normal, Time time) const { if (state_.raw_input_and_stylus_states.empty()) @@ -99,33 +216,17 @@ Result StylusStateModeler::Query(Vec2 position, .orientation = -1, }; - int closest_segment_index = -1; - float min_distance = std::numeric_limits::infinity(); - float interp_value = 0; - 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_.use_stroke_normal_projection && stroke_normal.has_value()) { - 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) { - closest_segment_index = i; - min_distance = distance; - interp_value = param; - } - } + std::optional projection = + params_.use_stroke_normal_projection && stroke_normal.has_value() + ? ProjectAlongStrokeNormal(position, acceleration, time, + *stroke_normal, + state_.raw_input_and_stylus_states) + : ProjectToClosestPoint(position, state_.raw_input_and_stylus_states); - if (closest_segment_index < 0) { - const auto &state = + if (!projection.has_value()) { + // We didn't find an appropriate projection; fall back to projecting to the + // closest endpoint of the raw input polyline. + const auto &fallback_state = Distance(state_.raw_input_and_stylus_states.front().position, position) < Distance(state_.raw_input_and_stylus_states.back().position, @@ -133,37 +234,45 @@ Result StylusStateModeler::Query(Vec2 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, + .position = fallback_state.position, + .velocity = fallback_state.velocity, + .acceleration = fallback_state.acceleration, .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, + .pressure = + state_.received_unknown_pressure ? -1 : fallback_state.pressure, + .tilt = state_.received_unknown_tilt ? -1 : fallback_state.tilt, + .orientation = state_.received_unknown_orientation + ? -1 + : fallback_state.orientation, }; } - 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]; + auto from_state = + state_.raw_input_and_stylus_states[projection->segment_index]; + auto to_state = + state_.raw_input_and_stylus_states[projection->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), + .position = Interp(from_state.position, to_state.position, + projection->ratio_along_segment), + .velocity = Interp(from_state.velocity, to_state.velocity, + projection->ratio_along_segment), + .acceleration = Interp(from_state.acceleration, to_state.acceleration, + projection->ratio_along_segment), .time = time, - .pressure = - state_.received_unknown_pressure - ? -1 - : Interp(from_state.pressure, to_state.pressure, interp_value), + .pressure = state_.received_unknown_pressure + ? -1 + : Interp(from_state.pressure, to_state.pressure, + projection->ratio_along_segment), .tilt = state_.received_unknown_tilt ? -1 - : Interp(from_state.tilt, to_state.tilt, interp_value), - .orientation = state_.received_unknown_orientation - ? -1 - : InterpAngle(from_state.orientation, - to_state.orientation, interp_value), + : Interp(from_state.tilt, to_state.tilt, + projection->ratio_along_segment), + .orientation = + state_.received_unknown_orientation + ? -1 + : InterpAngle(from_state.orientation, to_state.orientation, + projection->ratio_along_segment), }; } diff --git a/ink_stroke_modeler/internal/stylus_state_modeler.h b/ink_stroke_modeler/internal/stylus_state_modeler.h index 35d982f..5dfaf44 100644 --- a/ink_stroke_modeler/internal/stylus_state_modeler.h +++ b/ink_stroke_modeler/internal/stylus_state_modeler.h @@ -58,15 +58,26 @@ class StylusStateModeler { // 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}. + // return {.position = {0, 0}, + // .velocity = {0, 0}, + // .acceleration = {0, 0}, + // .time = Time(0), + // .pressure = -1, + // .tilt = -1, + // .orientation = -1} // - // `stroke_normal` is only used if - // `project_to_segment_along_normal_is_enabled` is true in the params. + // `acceleration` and `stroke_normal` is only used if + // `use_stroke_normal_projection` is true in the params. // // Note: While this returns a `Result`, the return value does not represent an // end result, but merely a container to hold all the relevant values. - Result Query(Vec2 position, std::optional stroke_normal, - Time time) const; + Result Query(Vec2 position, Vec2 acceleration, + std::optional stroke_normal, Time time) const; + + // The number of input samples currently held. Exposed for testing. + int InputSampleCount() const { + return state_.raw_input_and_stylus_states.size(); + } // Saves the current state of the stylus state modeler. See comment on // StrokeModeler::Save() for more details. diff --git a/ink_stroke_modeler/internal/stylus_state_modeler_test.cc b/ink_stroke_modeler/internal/stylus_state_modeler_test.cc index 8e0e06a..4c7ba86 100644 --- a/ink_stroke_modeler/internal/stylus_state_modeler_test.cc +++ b/ink_stroke_modeler/internal/stylus_state_modeler_test.cc @@ -39,44 +39,48 @@ const Result kUnknownResult{.position = {0, 0}, .pressure = -1, .tilt = -1, .orientation = -1}; -constexpr StylusStateModelerParams kNormalProjectionParams{ - .max_input_samples = 10, +const StylusStateModelerParams kNormalProjectionParams{ .use_stroke_normal_projection = true, + .min_input_samples = 5, + .min_sample_duration = Duration(.3), }; TEST(StylusStateModelerTest, QueryEmpty) { StylusStateModeler modeler; - EXPECT_EQ(modeler.Query({0, 0}, std::optional({0, 1}), Time(0)), - kUnknownResult); - EXPECT_EQ(modeler.Query({-5, 3}, std::optional({0, 1}), Time(0.1)), + EXPECT_EQ(modeler.Query({0, 0}, {0, 0}, std::optional({0, 1}), Time(0)), kUnknownResult); + EXPECT_EQ( + modeler.Query({-5, 3}, {0, 0}, std::optional({0, 1}), Time(0.1)), + kUnknownResult); } TEST(StylusStateModelerTest, QuerySingleInput) { StylusStateModeler modeler; modeler.Update({0, 0}, Time(0), {.pressure = 0.75, .tilt = 0.75, .orientation = 0.75}); - EXPECT_THAT(modeler.Query({0, 0}, std::optional({0, 1}), Time(0)), - ResultNear( - { - .position = {0, 0}, - .velocity = {0, 0}, - .acceleration = {0, 0}, - .time = Time(0), - .pressure = .75, - .tilt = .75, - .orientation = .75, - }, - kTol, kAccelTol)); - EXPECT_THAT(modeler.Query({1, 1}, std::optional({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, kAccelTol)); + EXPECT_THAT( + modeler.Query({0, 0}, {0, 0}, std::optional({0, 1}), Time(0)), + ResultNear( + { + .position = {0, 0}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = .75, + .tilt = .75, + .orientation = .75, + }, + kTol, kAccelTol)); + EXPECT_THAT( + modeler.Query({1, 1}, {0, 0}, std::optional({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, kAccelTol)); } TEST(StylusStateModelerTest, QuerySingleInputWithNormalProjection) { @@ -84,27 +88,29 @@ TEST(StylusStateModelerTest, QuerySingleInputWithNormalProjection) { modeler.Reset(kNormalProjectionParams); modeler.Update({0, 0}, Time(0), {.pressure = 0.75, .tilt = 0.75, .orientation = 0.75}); - EXPECT_THAT(modeler.Query({0, 0}, std::optional({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, kAccelTol)); - EXPECT_THAT(modeler.Query({1, 1}, std::optional({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, kAccelTol)); + EXPECT_THAT( + modeler.Query({0, 0}, {1, 1}, std::optional({0, 1}), Time(0)), + ResultNear( + { + .position = {0, 0}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = .75, + .tilt = .75, + .orientation = .75, + }, + kTol, kAccelTol)); + EXPECT_THAT( + modeler.Query({1, 1}, {1, 1}, std::optional({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, kAccelTol)); } TEST(StylusStateModelerTest, QueryMultipleInputs) { @@ -118,41 +124,44 @@ TEST(StylusStateModelerTest, QueryMultipleInputs) { modeler.Update({3.5, 4}, Time(0.3), {.pressure = .2, .tilt = .2, .orientation = .2}); - EXPECT_THAT(modeler.Query({0, 2}, std::optional({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, kAccelTol)); - EXPECT_THAT(modeler.Query({1, 2}, std::optional({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, kAccelTol)); - EXPECT_THAT(modeler.Query({2, 1.5}, std::optional({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, kAccelTol)); - EXPECT_THAT(modeler.Query({2.5, 1.875}, std::optional({-0.25, 0.125}), - Time(0.2)), + EXPECT_THAT( + modeler.Query({0, 2}, {0, 0}, std::optional({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, kAccelTol)); + EXPECT_THAT( + modeler.Query({1, 2}, {0, 0}, std::optional({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, kAccelTol)); + EXPECT_THAT( + modeler.Query({2, 1.5}, {0, 0}, std::optional({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, kAccelTol)); + EXPECT_THAT(modeler.Query({2.5, 1.875}, {0, 0}, + std::optional({-0.25, 0.125}), Time(0.2)), ResultNear( { .position = {2.25, 2}, @@ -164,8 +173,8 @@ TEST(StylusStateModelerTest, QueryMultipleInputs) { .orientation = .6, }, kTol, kAccelTol)); - EXPECT_THAT(modeler.Query({2.5, 3.125}, std::optional({0.25, -0.125}), - Time(0.22)), + EXPECT_THAT(modeler.Query({2.5, 3.125}, {0, 0}, + std::optional({0.25, -0.125}), Time(0.22)), ResultNear( { .position = {2.75, 3}, @@ -177,44 +186,45 @@ TEST(StylusStateModelerTest, QueryMultipleInputs) { .orientation = .4, }, kTol, kAccelTol)); + EXPECT_THAT(modeler.Query({2.5, 4}, {0, 0}, std::optional({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, kAccelTol)); + EXPECT_THAT(modeler.Query({3, 4}, {0, 0}, std::optional({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, kAccelTol)); EXPECT_THAT( - modeler.Query({2.5, 4}, std::optional({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, kAccelTol)); - EXPECT_THAT( - modeler.Query({3, 4}, std::optional({0.25, -0.25}), Time(0.29)), + modeler.Query({4, 4}, {0, 0}, std::optional({0, 1}), Time(0.31)), ResultNear( { - .position = {3.25, 3.75}, - .velocity = {7.5, 12.5}, - .acceleration = {-50, 25}, - .time = Time(0.29), - .pressure = .5, - .tilt = .15, - .orientation = .25, + .position = {3.5, 4}, + .velocity = {5, 5}, + .acceleration = {-50, -150}, + .time = Time(0.31), + .pressure = .2, + .tilt = .2, + .orientation = .2, }, kTol, kAccelTol)); - EXPECT_THAT(modeler.Query({4, 4}, std::optional({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, kAccelTol)); } TEST(StylusStateModelerTest, QueryMultipleInputsWithNormalProjection) { @@ -229,235 +239,204 @@ TEST(StylusStateModelerTest, QueryMultipleInputsWithNormalProjection) { modeler.Update({3.5, 4}, Time(0.3), {.pressure = .2, .tilt = .2, .orientation = .2}); - EXPECT_THAT(modeler.Query({0, 2}, std::optional({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, kAccelTol)); - EXPECT_THAT(modeler.Query({1, 2}, std::optional({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, kAccelTol)); - EXPECT_THAT(modeler.Query({2, 1.5}, std::optional({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, kAccelTol)); - EXPECT_THAT(modeler.Query({2.5, 1.875}, std::optional({-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, kAccelTol)); - EXPECT_THAT(modeler.Query({2.5, 3.125}, std::optional({0.3, -0.125}), + EXPECT_THAT( + modeler.Query({0, 2}, {0, 1}, std::optional({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, kAccelTol)); + EXPECT_THAT( + modeler.Query({1, 2}, {0, -1}, std::optional({-1, 1}), Time(0.1)), + ResultNear({.position = {1.5, 1.5}, + .velocity = {10, 0}, + .acceleration = {100, 0}, + .time = Time(0.1), + .pressure = .5, + .tilt = .6, + .orientation = .5}, + kTol, kAccelTol)); + EXPECT_THAT( + modeler.Query({2, 1.5}, {0, -1}, std::optional({-1, 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, kAccelTol)); + EXPECT_THAT( + modeler.Query({2.5, 2}, {0, 2}, std::optional({-3, 0}), Time(0.2)), + ResultNear({.position = {2.25, 2}, + .velocity = {13.75, 5}, + .acceleration = {100, 50}, + .time = Time(0.2), + .pressure = 0.65, + .tilt = 0.4, + .orientation = 0.6}, + kTol, kAccelTol)); + EXPECT_THAT(modeler.Query({2.5, 3}, {0, -1}, std::optional({-0.5, 0}), Time(0.22)), - ResultNear({.position = {2.75862, 3.01724}, - .velocity = {11.20690, 15.17241}, - .acceleration = {-1.72414, 151.72414}, + ResultNear({.position = {2.75, 3}, + .velocity = {11.25, 15}, + .acceleration = {0, 150}, .time = Time(0.22), - .pressure = 0.751724, - .tilt = 0.196552, - .orientation = 0.396552}, + .pressure = 0.75, + .tilt = 0.2, + .orientation = 0.4}, + kTol, kAccelTol)); + EXPECT_THAT(modeler.Query({3.25, 4}, {0, -2}, std::optional({0, 0.1}), + Time(0.29)), + ResultNear({.position = {3.25, 3.75}, + .velocity = {7.5, 12.5}, + .acceleration = {-50, 25}, + .time = Time(0.29), + .pressure = 0.5, + .tilt = 0.15, + .orientation = 0.25}, + kTol, kAccelTol)); + EXPECT_THAT( + modeler.Query({4, 4}, {0, -0.5}, std::optional({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, kAccelTol)); +} + +TEST(StylusStateModelerTest, + StrokeNormalProjectionChoosesCorrectTargetIfMultipleIntersections) { + StylusStateModeler modeler; + modeler.Reset(kNormalProjectionParams); + modeler.Update({0, 0}, Time(0), {.pressure = 0, .tilt = 0, .orientation = 0}); + modeler.Update({0, 4}, Time(0.1), + {.pressure = 0.2, .tilt = 0.2, .orientation = 0.2}); + modeler.Update({2, 4}, Time(0.2), + {.pressure = 0.4, .tilt = 0.4, .orientation = 0.4}); + modeler.Update({2, 0}, Time(0.3), + {.pressure = 0.6, .tilt = 0.6, .orientation = 0.6}); + + // If there are multiple intersections on the same side of the query, take the + // closest. + EXPECT_THAT(modeler.Query({-1, 2}, {0, -0.5}, std::optional({1, 0}), + Time(0.15)), + ResultNear({.position = {0, 2}, + .velocity = {0, 20}, + .acceleration = {0, 200}, + .time = Time(0.15), + .pressure = .1, + .tilt = .1, + .orientation = .1}, kTol, kAccelTol)); EXPECT_THAT( - modeler.Query({2.5, 4}, std::optional({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}, + modeler.Query({3, 2}, {0, -0.5}, std::optional({1, 0}), Time(0.15)), + ResultNear({.position = {2, 2}, + .velocity = {10, -20}, + .acceleration = {0, -400}, + .time = Time(0.15), + .pressure = .5, + .tilt = .5, + .orientation = .5}, kTol, kAccelTol)); + + // If there are intersections on either side of the query, take the one in the + // opposite direction of the acceleration. EXPECT_THAT( - modeler.Query({3, 4}, std::optional({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}, + modeler.Query({1, 2}, {1, -1}, std::optional({1, 0}), Time(0.15)), + ResultNear({.position = {0, 2}, + .velocity = {0, 20}, + .acceleration = {0, 200}, + .time = Time(0.15), + .pressure = .1, + .tilt = .1, + .orientation = .1}, + kTol, kAccelTol)); + EXPECT_THAT( + modeler.Query({1, 2}, {-1, -1}, std::optional({1, 0}), Time(0.15)), + ResultNear({.position = {2, 2}, + .velocity = {10, -20}, + .acceleration = {0, -400}, + .time = Time(0.15), + .pressure = .5, + .tilt = .5, + .orientation = .5}, kTol, kAccelTol)); - EXPECT_THAT(modeler.Query({4, 4}, std::optional({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, kAccelTol)); } -TEST(StylusStateModelerTest, QueryStaleInputsAreDiscarded) { +TEST(StylusStateModelerTest, StaleInputsAreDiscardedClosestPointProjection) { StylusStateModeler modeler; + modeler.Reset( + {.max_input_samples = 3, .use_stroke_normal_projection = false}); + + EXPECT_EQ(modeler.InputSampleCount(), 0); modeler.Update({1, 1}, Time(0), {.pressure = .6, .tilt = .5, .orientation = .4}); + EXPECT_EQ(modeler.InputSampleCount(), 1); modeler.Update({-1, 2}, Time(0.1), {.pressure = .3, .tilt = .7, .orientation = .6}); + EXPECT_EQ(modeler.InputSampleCount(), 2); modeler.Update({-4, 0}, Time(0.2), {.pressure = .9, .tilt = .7, .orientation = .3}); + EXPECT_EQ(modeler.InputSampleCount(), 3); modeler.Update({-6, -3}, Time(0.3), {.pressure = .4, .tilt = .3, .orientation = .5}); + EXPECT_EQ(modeler.InputSampleCount(), 3); 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_EQ(modeler.InputSampleCount(), 3); +} - EXPECT_THAT(modeler.Query({2, 0}, std::optional({0, 1}), Time(0)), - ResultNear( - { - .position = {1, 1}, - .velocity = {0, 0}, - .acceleration = {0, 0}, - .time = Time(0), - .pressure = .6, - .tilt = .5, - .orientation = .4, - }, - kTol, kAccelTol)); - EXPECT_THAT(modeler.Query({1, 3.5}, std::optional({-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, kAccelTol)); - EXPECT_THAT( - modeler.Query({-3, 17. / 6}, std::optional({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, kAccelTol)); +TEST(StylusStateModelerTest, StaleInputsAreDiscardedStrokeNormalProjection) { + StylusStateModeler modeler; + modeler.Reset({ + .use_stroke_normal_projection = true, + .min_input_samples = 3, + .min_sample_duration = Duration(.5), + }); - // 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}, std::optional({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, kAccelTol)); - EXPECT_THAT(modeler.Query({1, 3.5}, std::optional({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, kAccelTol)); - EXPECT_THAT( - modeler.Query({-3, 17. / 6}, std::optional({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, kAccelTol)); + EXPECT_EQ(modeler.InputSampleCount(), 0); + modeler.Update({1, 1}, Time(0), + {.pressure = .6, .tilt = .5, .orientation = .4}); + EXPECT_EQ(modeler.InputSampleCount(), 1); + modeler.Update({-1, 2}, Time(0.1), + {.pressure = .3, .tilt = .7, .orientation = .6}); + EXPECT_EQ(modeler.InputSampleCount(), 2); + modeler.Update({-4, 0}, Time(0.2), + {.pressure = .9, .tilt = .7, .orientation = .3}); + EXPECT_EQ(modeler.InputSampleCount(), 3); - // 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}, std::optional({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, kAccelTol)); - EXPECT_THAT(modeler.Query({1, 3.5}, std::optional({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, kAccelTol)); - EXPECT_THAT( - modeler.Query({-3, 17. / 6}, std::optional({-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, kAccelTol)); + // We've hit the minimum number of samples, but not the minimum duration. + modeler.Update({-6, -3}, Time(0.3), + {.pressure = .4, .tilt = .3, .orientation = .5}); + EXPECT_EQ(modeler.InputSampleCount(), 4); + + // Now we've hit the minimum duration as well, so we can drop the two oldest + // inputs. + modeler.Update({-5, -5}, Time(1), + {.pressure = .3, .tilt = .3, .orientation = .1}); + EXPECT_EQ(modeler.InputSampleCount(), 3); + + // Even though we meet the minimum duration with just two inputs, we don't + // drop below the minimum number of samples. + modeler.Update({-5, -5}, Time(2), + {.pressure = .3, .tilt = .3, .orientation = .1}); + EXPECT_EQ(modeler.InputSampleCount(), 3); } TEST(StylusStateModelerTest, QueryCyclicOrientationInterpolation) { @@ -470,17 +449,21 @@ TEST(StylusStateModelerTest, QueryCyclicOrientationInterpolation) { {.pressure = 0, .tilt = 0, .orientation = 1.6 * kPi}); EXPECT_NEAR( - modeler.Query({0, .25}, std::optional({1, 0}), Time(0)).orientation, + modeler.Query({0, .25}, {0, 0}, std::optional({1, 0}), Time(0)) + .orientation, 1.9 * kPi, kTol); EXPECT_NEAR( - modeler.Query({0, .75}, std::optional({1, 0}), Time(1)).orientation, + modeler.Query({0, .75}, {0, 0}, std::optional({1, 0}), Time(1)) + .orientation, .1 * kPi, kTol); - EXPECT_NEAR(modeler.Query({0, 1.25}, std::optional({1, 0}), Time(1.5)) - .orientation, - .05 * kPi, kTol); - EXPECT_NEAR(modeler.Query({0, 1.75}, std::optional({1, 0}), Time(2)) - .orientation, - 1.75 * kPi, kTol); + EXPECT_NEAR( + modeler.Query({0, 1.25}, {0, 0}, std::optional({1, 0}), Time(1.5)) + .orientation, + .05 * kPi, kTol); + EXPECT_NEAR( + modeler.Query({0, 1.75}, {0, 0}, std::optional({1, 0}), Time(2)) + .orientation, + 1.75 * kPi, kTol); } TEST(StylusStateModelerTest, QueryAndReset) { @@ -490,65 +473,69 @@ TEST(StylusStateModelerTest, QueryAndReset) { {.pressure = .4, .tilt = .9, .orientation = .1}); modeler.Update({7, 8}, Time(1), {.pressure = .1, .tilt = .2, .orientation = .5}); - EXPECT_THAT( - modeler.Query({10, 12}, std::optional({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, kAccelTol)); + EXPECT_THAT(modeler.Query({10, 12}, {0, 0}, std::optional({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, kAccelTol)); modeler.Reset(StylusStateModelerParams{}); - EXPECT_EQ(modeler.Query({10, 12}, std::optional({0.5, -0.5}), Time(0)), + EXPECT_EQ(modeler.Query({10, 12}, {0, 0}, std::optional({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}, std::optional({-7, -3}), Time(2)), - ResultNear( - { - .position = {-1, 4}, - .velocity = {0, 0}, - .acceleration = {0, 0}, - .time = Time(2), - .pressure = .4, - .tilt = .6, - .orientation = .8, - }, - kTol, kAccelTol)); + EXPECT_THAT( + modeler.Query({6, 7}, {0, 0}, std::optional({-7, -3}), Time(2)), + ResultNear( + { + .position = {-1, 4}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(2), + .pressure = .4, + .tilt = .6, + .orientation = .8, + }, + kTol, kAccelTol)); modeler.Update({-3, 0}, Time(3), {.pressure = .7, .tilt = .2, .orientation = .5}); - EXPECT_THAT(modeler.Query({-2, 2}, std::optional({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, kAccelTol)); - EXPECT_THAT(modeler.Query({0, 5}, std::optional({-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, kAccelTol)); + EXPECT_THAT( + modeler.Query({-2, 2}, {0, 0}, std::optional({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, kAccelTol)); + EXPECT_THAT( + modeler.Query({0, 5}, {0, 0}, std::optional({-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, kAccelTol)); } TEST(StylusStateModelerTest, UpdateWithUnknownState) { @@ -558,42 +545,46 @@ TEST(StylusStateModelerTest, UpdateWithUnknownState) { {.pressure = .1, .tilt = .2, .orientation = .3}); modeler.Update({2, 3}, Time(1), {.pressure = .3, .tilt = .4, .orientation = .5}); - EXPECT_THAT(modeler.Query({2, 2}, std::optional({-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, kAccelTol)); + EXPECT_THAT( + modeler.Query({2, 2}, {0, 0}, std::optional({-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, kAccelTol)); modeler.Update({5, 5}, Time(2), kUnknownState); - EXPECT_EQ(modeler.Query({5, 5}, std::optional({-0.5, 0.5}), Time(1)), - kUnknownResult); + EXPECT_EQ( + modeler.Query({5, 5}, {0, 0}, std::optional({-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}, std::optional({-0.5, 0.5}), Time(2)), - kUnknownResult); + EXPECT_EQ( + modeler.Query({1, 2}, {0, 0}, std::optional({-0.5, 0.5}), Time(2)), + kUnknownResult); modeler.Update({-1, 3}, Time(4), kUnknownState); - EXPECT_EQ(modeler.Query({7, 9}, std::optional({-0.5, 0.5}), Time(3)), - kUnknownResult); + EXPECT_EQ( + modeler.Query({7, 9}, {0, 0}, std::optional({-0.5, 0.5}), Time(3)), + kUnknownResult); modeler.Reset(StylusStateModelerParams{}); modeler.Update({3, 3}, Time(5), {.pressure = .7, .tilt = .6, .orientation = .5}); - EXPECT_THAT( - modeler.Query({3, 3}, std::optional({-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, kAccelTol)); + EXPECT_THAT(modeler.Query({3, 3}, {0, 0}, std::optional({-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, kAccelTol)); } TEST(StylusStateModelerTest, StrokeNormalIgnored) { @@ -603,10 +594,11 @@ TEST(StylusStateModelerTest, StrokeNormalIgnored) { {.pressure = .4, .tilt = .9, .orientation = .1}); modeler.Update({7, 8}, Time(1), {.pressure = .1, .tilt = .2, .orientation = .5}); - EXPECT_THAT( - modeler.Query({5, 7}, std::optional({0.5, -0.5}), Time(0.2)), - ResultNear(modeler.Query({5, 7}, std::optional({0, 1}), Time(0.2)), - kTol, kAccelTol)); + EXPECT_THAT(modeler.Query({5, 7}, {0, 0}, std::optional({0.5, -0.5}), + Time(0.2)), + ResultNear(modeler.Query({5, 7}, {0, 0}, + std::optional({0, 1}), Time(0.2)), + kTol, kAccelTol)); } TEST(StylusStateModelerTest, ModelPressureOnly) { @@ -614,33 +606,35 @@ TEST(StylusStateModelerTest, ModelPressureOnly) { modeler.Update({0, 0}, Time(0), {.pressure = .5, .tilt = -2, .orientation = -.1}); - EXPECT_THAT(modeler.Query({1, 1}, std::optional({0, 1}), Time(0)), - ResultNear( - { - .position = {0, 0}, - .velocity = {0, 0}, - .acceleration = {0, 0}, - .time = Time(0), - .pressure = .5, - .tilt = -1, - .orientation = -1, - }, - kTol, kAccelTol)); + EXPECT_THAT( + modeler.Query({1, 1}, {0, 0}, std::optional({0, 1}), Time(0)), + ResultNear( + { + .position = {0, 0}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = .5, + .tilt = -1, + .orientation = -1, + }, + kTol, kAccelTol)); modeler.Update({2, 0}, Time(1), {.pressure = .7, .tilt = -2, .orientation = -.1}); - EXPECT_THAT(modeler.Query({1, 1}, std::optional({0, -1}), Time(1)), - ResultNear( - { - .position = {1, 0}, - .velocity = {1, 0}, - .acceleration = {1, 0}, - .time = Time(1), - .pressure = .6, - .tilt = -1, - .orientation = -1, - }, - kTol, kAccelTol)); + EXPECT_THAT( + modeler.Query({1, 1}, {0, 0}, std::optional({0, -1}), Time(1)), + ResultNear( + { + .position = {1, 0}, + .velocity = {1, 0}, + .acceleration = {1, 0}, + .time = Time(1), + .pressure = .6, + .tilt = -1, + .orientation = -1, + }, + kTol, kAccelTol)); } TEST(StylusStateModelerTest, ModelTiltOnly) { @@ -648,33 +642,35 @@ TEST(StylusStateModelerTest, ModelTiltOnly) { modeler.Update({0, 0}, Time(0), {.pressure = -2, .tilt = .5, .orientation = -.1}); - EXPECT_THAT(modeler.Query({1, 1}, std::optional({0, 1}), Time(0)), - ResultNear( - { - .position = {0, 0}, - .velocity = {0, 0}, - .acceleration = {0, 0}, - .time = Time(0), - .pressure = -1, - .tilt = .5, - .orientation = -1, - }, - kTol, kAccelTol)); + EXPECT_THAT( + modeler.Query({1, 1}, {0, 0}, std::optional({0, 1}), Time(0)), + ResultNear( + { + .position = {0, 0}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = -1, + .tilt = .5, + .orientation = -1, + }, + kTol, kAccelTol)); modeler.Update({2, 0}, Time(1), {.pressure = -2, .tilt = .3, .orientation = -.1}); - EXPECT_THAT(modeler.Query({1, 1}, std::optional({0, -1}), Time(1)), - ResultNear( - { - .position = {1, 0}, - .velocity = {1, 0}, - .acceleration = {1, 0}, - .time = Time(1), - .pressure = -1, - .tilt = .4, - .orientation = -1, - }, - kTol, kAccelTol)); + EXPECT_THAT( + modeler.Query({1, 1}, {0, 0}, std::optional({0, -1}), Time(1)), + ResultNear( + { + .position = {1, 0}, + .velocity = {1, 0}, + .acceleration = {1, 0}, + .time = Time(1), + .pressure = -1, + .tilt = .4, + .orientation = -1, + }, + kTol, kAccelTol)); } TEST(StylusStateModelerTest, ModelOrientationOnly) { @@ -682,33 +678,35 @@ TEST(StylusStateModelerTest, ModelOrientationOnly) { modeler.Update({0, 0}, Time(0), {.pressure = -2, .tilt = -.1, .orientation = 1}); - EXPECT_THAT(modeler.Query({1, 1}, std::optional({0, 1}), Time(0)), - ResultNear( - { - .position = {0, 0}, - .velocity = {0, 0}, - .acceleration = {0, 0}, - .time = Time(0), - .pressure = -1, - .tilt = -1, - .orientation = 1, - }, - kTol, kAccelTol)); + EXPECT_THAT( + modeler.Query({1, 1}, {0, 0}, std::optional({0, 1}), Time(0)), + ResultNear( + { + .position = {0, 0}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = -1, + .tilt = -1, + .orientation = 1, + }, + kTol, kAccelTol)); modeler.Update({2, 0}, Time(1), {.pressure = -2, .tilt = -.3, .orientation = 2}); - EXPECT_THAT(modeler.Query({1, 1}, std::optional({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, kAccelTol)); + EXPECT_THAT( + modeler.Query({1, 1}, {0, 0}, std::optional({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, kAccelTol)); } TEST(StylusStateModelerTest, DropFieldsOneByOne) { @@ -716,97 +714,104 @@ TEST(StylusStateModelerTest, DropFieldsOneByOne) { modeler.Update({0, 0}, Time(0), {.pressure = .5, .tilt = .5, .orientation = .5}); - EXPECT_THAT(modeler.Query({1, 0}, std::optional({0, 1}), Time(0)), - ResultNear( - { - .position = {0, 0}, - .velocity = {0, 0}, - .acceleration = {0, 0}, - .time = Time(0), - .pressure = .5, - .tilt = .5, - .orientation = .5, - }, - kTol, kAccelTol)); + EXPECT_THAT( + modeler.Query({1, 0}, {0, 0}, std::optional({0, 1}), Time(0)), + ResultNear( + { + .position = {0, 0}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = .5, + .tilt = .5, + .orientation = .5, + }, + kTol, kAccelTol)); modeler.Update({2, 0}, Time(1), {.pressure = .3, .tilt = .7, .orientation = -1}); - EXPECT_THAT(modeler.Query({1, 0}, std::optional({0, 1}), Time(1)), - ResultNear( - { - .position = {1, 0}, - .velocity = {1, 0}, - .acceleration = {1, 0}, - .time = Time(1), - .pressure = .4, - .tilt = .6, - .orientation = -1, - }, - kTol, kAccelTol)); + EXPECT_THAT( + modeler.Query({1, 0}, {0, 0}, std::optional({0, 1}), Time(1)), + ResultNear( + { + .position = {1, 0}, + .velocity = {1, 0}, + .acceleration = {1, 0}, + .time = Time(1), + .pressure = .4, + .tilt = .6, + .orientation = -1, + }, + kTol, kAccelTol)); modeler.Update({4, 0}, Time(2), {.pressure = .1, .tilt = -1, .orientation = 1}); - EXPECT_THAT(modeler.Query({3, 0}, std::optional({0, 1}), Time(2)), - ResultNear( - { - .position = {3, 0}, - .velocity = {2, 0}, - .acceleration = {1, 0}, - .time = Time(2), - .pressure = .2, - .tilt = -1, - .orientation = -1, - }, - kTol, kAccelTol)); + EXPECT_THAT( + modeler.Query({3, 0}, {0, 0}, std::optional({0, 1}), Time(2)), + ResultNear( + { + .position = {3, 0}, + .velocity = {2, 0}, + .acceleration = {1, 0}, + .time = Time(2), + .pressure = .2, + .tilt = -1, + .orientation = -1, + }, + kTol, kAccelTol)); modeler.Update({6, 0}, Time(3), {.pressure = -1, .tilt = .2, .orientation = 0}); - EXPECT_THAT(modeler.Query({5, 0}, std::optional({0, 1}), Time(3)), - ResultNear( - { - .position = {0, 0}, - .velocity = {0, 0}, - .acceleration = {0, 0}, - .time = Time(0), - .pressure = -1, - .tilt = -1, - .orientation = -1, - }, - kTol, kAccelTol)); + EXPECT_THAT( + modeler.Query({5, 0}, {0, 0}, std::optional({0, 1}), Time(3)), + ResultNear( + { + .position = {0, 0}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = -1, + .tilt = -1, + .orientation = -1, + }, + kTol, kAccelTol)); modeler.Update({8, 0}, Time(4), {.pressure = .3, .tilt = .4, .orientation = .5}); - EXPECT_THAT(modeler.Query({7, 0}, std::optional({0, 1}), Time(4)), - ResultNear( - { - .position = {0, 0}, - .velocity = {0, 0}, - .acceleration = {0, 0}, - .time = Time(0), - .pressure = -1, - .tilt = -1, - .orientation = -1, - }, - kTol, kAccelTol)); + EXPECT_THAT( + modeler.Query({7, 0}, {0, 0}, std::optional({0, 1}), Time(4)), + ResultNear( + { + .position = {0, 0}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = -1, + .tilt = -1, + .orientation = -1, + }, + kTol, kAccelTol)); modeler.Reset(StylusStateModelerParams{}); - EXPECT_THAT(modeler.Query({1, 0}, std::optional({0, 1}), Time(0)), - ResultNear(kUnknownResult, kTol, kAccelTol)); + EXPECT_THAT( + modeler.Query({1, 0}, {0, 0}, std::optional({0, 1}), Time(0)), + ResultNear(kUnknownResult, kTol, kAccelTol)); modeler.Update({0, 0}, Time(5), {.pressure = .1, .tilt = .8, .orientation = .3}); - EXPECT_THAT(modeler.Query({1, 0}, std::optional({0, 1}), Time(5)), - ResultNear( - { - .position = {0, 0}, - .velocity = {0, 0}, - .acceleration = {0, 0}, - .time = Time(5), - .pressure = .1, - .tilt = .8, - .orientation = .3, - }, - kTol, kAccelTol)); + EXPECT_THAT( + modeler.Query({1, 0}, {0, 0}, std::optional({0, 1}), Time(5)), + ResultNear( + { + .position = {0, 0}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(5), + .pressure = .1, + .tilt = .8, + .orientation = .3, + }, + kTol, kAccelTol)); } TEST(StylusStateModelerTest, SaveAndRestore) { @@ -832,34 +837,36 @@ TEST(StylusStateModelerTest, SaveAndRestore) { modeler.Update({-10, -2}, Time(9), {.pressure = .7, .tilt = .3, .orientation = .2}); - ASSERT_THAT(modeler.Query({2, 0}, std::optional({0, 1}), Time(0)), - ResultNear( - { - .position = {1, 1}, - .velocity = {0, 0}, - .acceleration = {0, 0}, - .time = Time(0), - .pressure = .6, - .tilt = .5, - .orientation = .4, - }, - kTol, kAccelTol)); + ASSERT_THAT( + modeler.Query({2, 0}, {0, 0}, std::optional({0, 1}), Time(0)), + ResultNear( + { + .position = {1, 1}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = .6, + .tilt = .5, + .orientation = .4, + }, + kTol, kAccelTol)); // Calling restore with no save should have no effect. modeler.Restore(); - EXPECT_THAT(modeler.Query({2, 0}, std::optional({0, 1}), Time(0)), - ResultNear( - { - .position = {1, 1}, - .velocity = {0, 0}, - .acceleration = {0, 0}, - .time = Time(0), - .pressure = .6, - .tilt = .5, - .orientation = .4, - }, - kTol, kAccelTol)); + EXPECT_THAT( + modeler.Query({2, 0}, {0, 0}, std::optional({0, 1}), Time(0)), + ResultNear( + { + .position = {1, 1}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = .6, + .tilt = .5, + .orientation = .4, + }, + kTol, kAccelTol)); modeler.Save(); @@ -868,33 +875,35 @@ TEST(StylusStateModelerTest, SaveAndRestore) { {.pressure = .6, .tilt = .8, .orientation = .9}); modeler.Update({-8, 0}, Time(11), {.pressure = .6, .tilt = .8, .orientation = .9}); - EXPECT_THAT(modeler.Query({2, 0}, std::optional({0, 1}), Time(0)), - ResultNear( - { - .position = {-4, 0}, - .velocity = {-3, -2}, - .acceleration = {-1, -3}, - .time = Time(0), - .pressure = .9, - .tilt = .7, - .orientation = .3, - }, - kTol, kAccelTol)); + EXPECT_THAT( + modeler.Query({2, 0}, {0, 0}, std::optional({0, 1}), Time(0)), + ResultNear( + { + .position = {-4, 0}, + .velocity = {-3, -2}, + .acceleration = {-1, -3}, + .time = Time(0), + .pressure = .9, + .tilt = .7, + .orientation = .3, + }, + kTol, kAccelTol)); // Restoring should revert the updates. modeler.Restore(); - EXPECT_THAT(modeler.Query({2, 0}, std::optional({0, 1}), Time(0)), - ResultNear( - { - .position = {1, 1}, - .velocity = {0, 0}, - .acceleration = {0, 0}, - .time = Time(0), - .pressure = .6, - .tilt = .5, - .orientation = .4, - }, - kTol, kAccelTol)); + EXPECT_THAT( + modeler.Query({2, 0}, {0, 0}, std::optional({0, 1}), Time(0)), + ResultNear( + { + .position = {1, 1}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = .6, + .tilt = .5, + .orientation = .4, + }, + kTol, kAccelTol)); // Restoring should not have cleared the saved state, so we can repeat the // action. @@ -902,62 +911,66 @@ TEST(StylusStateModelerTest, SaveAndRestore) { {.pressure = .6, .tilt = .8, .orientation = .9}); modeler.Update({-8, 0}, Time(13), {.pressure = .6, .tilt = .8, .orientation = .9}); - EXPECT_THAT(modeler.Query({2, 0}, std::optional({0, 1}), Time(0)), - ResultNear( - { - .position = {-4, 0}, - .velocity = {-3, -2}, - .acceleration = {-1, -3}, - .time = Time(0), - .pressure = .9, - .tilt = .7, - .orientation = .3, - }, - kTol, kAccelTol)); + EXPECT_THAT( + modeler.Query({2, 0}, {0, 0}, std::optional({0, 1}), Time(0)), + ResultNear( + { + .position = {-4, 0}, + .velocity = {-3, -2}, + .acceleration = {-1, -3}, + .time = Time(0), + .pressure = .9, + .tilt = .7, + .orientation = .3, + }, + kTol, kAccelTol)); modeler.Restore(); - EXPECT_THAT(modeler.Query({2, 0}, std::optional({0, 1}), Time(0)), - ResultNear( - { - .position = {1, 1}, - .velocity = {0, 0}, - .acceleration = {0, 0}, - .time = Time(0), - .pressure = .6, - .tilt = .5, - .orientation = .4, - }, - kTol, kAccelTol)); + EXPECT_THAT( + modeler.Query({2, 0}, {0, 0}, std::optional({0, 1}), Time(0)), + ResultNear( + { + .position = {1, 1}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = .6, + .tilt = .5, + .orientation = .4, + }, + kTol, kAccelTol)); // Calling Reset should clear the save point so that calling Restore should // have no effect. modeler.Reset(StylusStateModelerParams{}); modeler.Update({-1, 4}, Time(14), {.pressure = .4, .tilt = .6, .orientation = .8}); - EXPECT_THAT(modeler.Query({6, 7}, std::optional({0, 1}), Time(0)), - ResultNear( - { - .position = {-1, 4}, - .velocity = {0, 0}, - .acceleration = {0, 0}, - .time = Time(0), - .pressure = .4, - .tilt = .6, - .orientation = .8, - }, - kTol, kAccelTol)); + EXPECT_THAT( + modeler.Query({6, 7}, {0, 0}, std::optional({0, 1}), Time(0)), + ResultNear( + { + .position = {-1, 4}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = .4, + .tilt = .6, + .orientation = .8, + }, + kTol, kAccelTol)); modeler.Restore(); - EXPECT_THAT(modeler.Query({6, 7}, std::optional({0, 1}), Time(0)), - ResultNear( - { - .position = {-1, 4}, - .velocity = {0, 0}, - .acceleration = {0, 0}, - .time = Time(0), - .pressure = .4, - .tilt = .6, - .orientation = .8, - }, - kTol, kAccelTol)); + EXPECT_THAT( + modeler.Query({6, 7}, {0, 0}, std::optional({0, 1}), Time(0)), + ResultNear( + { + .position = {-1, 4}, + .velocity = {0, 0}, + .acceleration = {0, 0}, + .time = Time(0), + .pressure = .4, + .tilt = .6, + .orientation = .8, + }, + kTol, kAccelTol)); } } // namespace diff --git a/ink_stroke_modeler/params.cc b/ink_stroke_modeler/params.cc index 61a6aea..d1ca809 100644 --- a/ink_stroke_modeler/params.cc +++ b/ink_stroke_modeler/params.cc @@ -137,8 +137,18 @@ absl::Status ValidateSamplingParams(const SamplingParams& params) { absl::Status ValidateStylusStateModelerParams( const StylusStateModelerParams& params) { - return ValidateGreaterThanZero(params.max_input_samples, - "StylusStateModelerParams::max_input_samples"); + if (params.use_stroke_normal_projection) { + RETURN_IF_ERROR( + ValidateGreaterThanZero(params.min_input_samples, + "StylusStateModelerParams::min_input_samples")); + return ValidateGreaterThanZero( + params.min_sample_duration.Value(), + "StylusStateModelerParams::min_sample_duration"); + } else { + return ValidateGreaterThanZero( + params.max_input_samples, + "StylusStateModelerParams::max_input_samples"); + } } absl::Status ValidateWobbleSmootherParams(const WobbleSmootherParams& params) { diff --git a/ink_stroke_modeler/params.h b/ink_stroke_modeler/params.h index 1a624ab..f03e837 100644 --- a/ink_stroke_modeler/params.h +++ b/ink_stroke_modeler/params.h @@ -144,22 +144,48 @@ struct SamplingParams { double max_estimated_angle_to_traverse_per_input = -1; }; -// These parameters are used modeling the state of the stylus once the position -// has been modeled. +// These parameters are used modeling the non-positional state of the stylus +// (i.e. pressure, tilt, and orientation) once the position has been modeled. +// +// To calculate the non-positional state, we project the modeled position of the +// tip, to a polyline made up of the most recent raw inputs, and then +// interpolate pressure, tilt, and orientation along that raw input polyline. +// These parameters determine the projection method, and how many raw input +// samples to include in the polyline. struct StylusStateModelerParams { - // The maximum number of raw inputs to look at when finding the relevant - // states for interpolation. + // This determines the number of recent raw input samples to use when + // 'use_stroke_normal_projection` is false; we accumulate `max_input_samples` + // samples, then discard old samples as we receive new inputs. + // If `use_stroke_normal_projection` is false, this must be greater than zero. + // If `use_stroke_normal_projection` is true, this will be ignored, and + // `min_input_samples` and `min_sample_duration` will be used instead. int max_input_samples = 10; - // This toggles between the two projection methods available. If - // `use_stroke_normal_projection` is false, a call to - // `StylusStateModeler::Query` will base its calculations on the nearest point - // for the closest segment. If `use_stroke_normal_projection` is true, it use - // the stroke normal of the polyline for its calculations. + // This determines the method used to project to the raw input polyline. + // * If false, we take the point on the polyline closest to the modeled tip + // position. + // * If true, we cast a pair of rays normal to the stroke direction from the + // modeled tip point and find the intersection with the raw input polyline. + // If multiple intersections are found, we use a heuristic to determine the + // best choice. // We recommend enabling this in order to get increased accuracy for pressure, // tilt, and orientation; however, this defaults to false to preserve behavior // for existing uses. bool use_stroke_normal_projection = false; + + // These determine the number of recent raw input samples to use when + // `use_stroke_normal_projection` is true: we accumulate samples until both of + // the following conditions are true: + // * We have at least `min_input_samples` samples + // * The difference in `time` between the first and last sample is greater + // than `min_sample_duration` + // As we receive additional raw inputs, we discard old samples once they are + // no longer required to maintain those conditions. + // If `use_stroke_normal_projection` is true, both of these must be >= zero. + // If `use_stroke_normal_projection` is false, these will be ignored, and + // `max_input_samples` will be used instead. + int min_input_samples = -1; + Duration min_sample_duration{-1}; }; // These parameters are used for applying smoothing to the input to reduce diff --git a/ink_stroke_modeler/params_test.cc b/ink_stroke_modeler/params_test.cc index e978283..857a32f 100644 --- a/ink_stroke_modeler/params_test.cc +++ b/ink_stroke_modeler/params_test.cc @@ -59,8 +59,10 @@ const StrokeModelParams kGoodStrokeModelParams{ .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, - .use_stroke_normal_projection = true}, + .stylus_state_modeler_params{.max_input_samples = -1, + .use_stroke_normal_projection = true, + .min_input_samples = 10, + .min_sample_duration = Duration(0.05)}, .prediction_params = StrokeEndPredictorParams{}}; TEST(ParamsTest, ValidatePositionModelerParams) { @@ -169,12 +171,34 @@ TEST(ParamsTest, ValidateSamplingParams) { .ok()); } -TEST(ParamsTest, ValidateStylusStateModelerParams) { +TEST(ParamsTest, ValidateStylusStateModelerParamsDefaultProjection) { EXPECT_TRUE(ValidateStylusStateModelerParams({.max_input_samples = 1}).ok()); EXPECT_EQ(ValidateStylusStateModelerParams({.max_input_samples = 0}).code(), absl::StatusCode::kInvalidArgument); } +TEST(ParamsTest, ValidateStylusStateModelerParamsStrokeNormalProjection) { + EXPECT_TRUE( + ValidateStylusStateModelerParams({.max_input_samples = -1, + .use_stroke_normal_projection = true, + .min_input_samples = 2, + .min_sample_duration = Duration(.05)}) + .ok()); + EXPECT_EQ( + ValidateStylusStateModelerParams({.max_input_samples = -1, + .use_stroke_normal_projection = true, + .min_input_samples = 0, + .min_sample_duration = Duration(0.05)}) + .code(), + absl::StatusCode::kInvalidArgument); + EXPECT_EQ( + ValidateStylusStateModelerParams({.max_input_samples = -1, + .use_stroke_normal_projection = false, + .min_input_samples = 10, + .min_sample_duration = Duration(-0.1)}) + .code(), + absl::StatusCode::kInvalidArgument); +} TEST(ParamsTest, ValidateWobbleSmootherParams) { EXPECT_TRUE( @@ -313,7 +337,7 @@ TEST(ParamsTest, ValidateStrokeModelParams) { } { auto bad_params = kGoodStrokeModelParams; - bad_params.stylus_state_modeler_params.max_input_samples = 0; + bad_params.stylus_state_modeler_params.min_input_samples = 0; EXPECT_EQ(ValidateStrokeModelParams(bad_params).code(), absl::StatusCode::kInvalidArgument); } diff --git a/ink_stroke_modeler/stroke_modeler.cc b/ink_stroke_modeler/stroke_modeler.cc index dd9a374..bbc5a7e 100644 --- a/ink_stroke_modeler/stroke_modeler.cc +++ b/ink_stroke_modeler/stroke_modeler.cc @@ -78,8 +78,9 @@ void ModelStylus( loop_contraction_mitigation_modeler.GetInterpolationValue(); for (const auto &tip_state : tip_states) { std::optional stroke_normal = GetStrokeNormal(tip_state, prev_time); - Result projected_state = stylus_state_modeler.Query( - tip_state.position, stroke_normal, tip_state.time); + Result projected_state = + stylus_state_modeler.Query(tip_state.position, tip_state.acceleration, + stroke_normal, tip_state.time); Result modeled_state = MakeResultFromTipState(tip_state, projected_state); result.push_back( InterpResult(projected_state, modeled_state, interp_value)); diff --git a/ink_stroke_modeler/stroke_modeler_fuzz_test.cc b/ink_stroke_modeler/stroke_modeler_fuzz_test.cc index 96ca225..d794343 100644 --- a/ink_stroke_modeler/stroke_modeler_fuzz_test.cc +++ b/ink_stroke_modeler/stroke_modeler_fuzz_test.cc @@ -31,6 +31,12 @@ ArbitraryLoopContractionMitigationParameters() { fuzztest::Arbitrary()); } +fuzztest::Domain ArbitraryStylusStateModelerParams() { + return fuzztest::StructOf( + fuzztest::Arbitrary(), fuzztest::Arbitrary(), + fuzztest::Arbitrary(), ArbitraryDuration()); +} + fuzztest::Domain ArbitraryStrokeModelParams() { return fuzztest::StructOf( fuzztest::StructOf( @@ -44,7 +50,7 @@ fuzztest::Domain ArbitraryStrokeModelParams() { fuzztest::Arbitrary(), /*max_outputs_per_call*/ fuzztest::InRange(1000, 100000), fuzztest::Arbitrary()), - fuzztest::Arbitrary(), + ArbitraryStylusStateModelerParams(), fuzztest::VariantOf( fuzztest::Arbitrary(), fuzztest::StructOf( diff --git a/ink_stroke_modeler/stroke_modeler_test.cc b/ink_stroke_modeler/stroke_modeler_test.cc index c05b8a4..760ee9a 100644 --- a/ink_stroke_modeler/stroke_modeler_test.cc +++ b/ink_stroke_modeler/stroke_modeler_test.cc @@ -365,6 +365,8 @@ TEST(StrokeModelerTest, InputRateSlowerThanMinOutputRateNormalProjection) { StrokeModeler modeler; StrokeModelParams params = kDefaultParams; params.stylus_state_modeler_params.use_stroke_normal_projection = true; + params.stylus_state_modeler_params.min_input_samples = 10; + params.stylus_state_modeler_params.min_sample_duration = Duration(0.334); ASSERT_TRUE(modeler.Reset(params).ok()); Time time{0}; @@ -650,6 +652,8 @@ TEST(StrokeModelerTest, const Duration kDeltaTime{1. / 30}; StrokeModelParams params = kDefaultParams; params.stylus_state_modeler_params.use_stroke_normal_projection = true; + params.stylus_state_modeler_params.min_input_samples = 10; + params.stylus_state_modeler_params.min_sample_duration = Duration(0.334); params.position_modeler_params.loop_contraction_mitigation_params = PositionModelerParams::LoopContractionMitigationParameters{ .is_enabled = true,