aboutsummaryrefslogtreecommitdiff
path: root/ink_stroke_modeler/internal/position_modeler.h
blob: c3276e895ec817612b7cff54f3121199d222e20c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
/*
 * 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.
 */

#ifndef INK_STROKE_MODELER_INTERNAL_POSITION_MODELER_H_
#define INK_STROKE_MODELER_INTERNAL_POSITION_MODELER_H_

#include "ink_stroke_modeler/internal/internal_types.h"
#include "ink_stroke_modeler/internal/utils.h"
#include "ink_stroke_modeler/params.h"
#include "ink_stroke_modeler/types.h"

namespace ink {
namespace stroke_model {

// This class models the movement of the pen tip based on the laws of motion.
// The pen tip is represented as a mass, connected by a spring to a moving
// anchor; as the anchor moves, it drags the pen tip along behind it.
class PositionModeler {
 public:
  void Reset(const TipState& state, PositionModelerParams params) {
    state_ = state;
    params_ = params;
  }

  // Given the position of the anchor and the time, updates the model and
  // returns the state of the pen tip.
  TipState Update(Vec2 anchor_position, Time time) {
    Duration delta_time = time - state_.time;
    float float_delta = delta_time.Value();
    auto acceleration =
        (anchor_position - state_.position) / params_.spring_mass_constant -
        params_.drag_constant * state_.velocity;
    state_.velocity += float_delta * acceleration;
    state_.position += float_delta * state_.velocity;
    state_.time = time;

    return state_;
  }

  const TipState& CurrentState() const { return state_; }
  const PositionModelerParams& Params() const { return params_; }

  // This helper function linearly interpolates between the between the start
  // and end anchor position and time, updating the model at each step and
  // storing the result in the given output iterator.
  //
  // NOTE: Because the expected use case is to repeatedly call this function on
  // a sequence of anchor positions/times, the start position/time is not sent
  // to the model. This prevents us from duplicating those inputs, but it does
  // mean that the first input must be provided on its own, via either Reset()
  // or Update(). This also means that the interpolation values are
  // (1 ... n) / n, as opposed to (0 ... (n - 1)) / (n - 1).
  //
  // Template parameter OutputIt is expected to be an output iterator over
  // TipState.
  template <typename OutputIt>
  void UpdateAlongLinearPath(Vec2 start_anchor_position, Time start_time,
                             Vec2 end_anchor_position, Time end_time,
                             int n_samples, OutputIt output) {
    for (int i = 1; i <= n_samples; ++i) {
      auto interp_value = static_cast<float>(i) / n_samples;
      auto position =
          Interp(start_anchor_position, end_anchor_position, interp_value);
      auto time = Interp(start_time, end_time, interp_value);
      *output++ = Update(position, time);
    }
  }

  // This helper function models the end of the stroke, by repeatedly updating
  // with the final anchor position. It attempts to stop at the closest point to
  // the anchor, by checking if it has overshot, and retrying with successively
  // smaller time steps.
  //
  // It halts when any of these three conditions is met:
  // - It has taken more than max_iterations steps (including discarded steps)
  // - The distance between the current state and the anchor is less than
  //   stop_distance
  // - The distance between the previous state and the current state is less
  //   than stop_distance
  //
  // Template parameter OutputIt is expected to be an output iterator over
  // TipState.
  template <typename OutputIt>
  void ModelEndOfStroke(Vec2 anchor_position, Duration delta_time,
                        int max_iterations, float stop_distance,
                        OutputIt output) {
    for (int i = 0; i < max_iterations; ++i) {
      // The call to Update modifies the state, so we store a copy of the
      // previous state so we can retry with a smaller step if necessary.
      const TipState previous_state = state_;
      TipState candidate =
          Update(anchor_position, previous_state.time + delta_time);
      if (Distance(previous_state.position, candidate.position) <
          stop_distance) {
        // We're no longer making any significant progress, which means that
        // we're about as close as we can get without looping around.
        return;
      }

      float closest_t = NearestPointOnSegment(
          previous_state.position, candidate.position, anchor_position);
      if (closest_t < 1) {
        // We're overshot the anchor, retry with a smaller step.
        delta_time *= .5;
        state_ = previous_state;
        continue;
      }
      *output++ = candidate;

      if (Distance(candidate.position, anchor_position) < stop_distance) {
        // We're within tolerance of the anchor.
        return;
      }
    }
  }

 private:
  PositionModelerParams params_;
  TipState state_;
};

}  // namespace stroke_model
}  // namespace ink

#endif  // INK_STROKE_MODELER_INTERNAL_POSITION_MODELER_H_