Flutter Impeller
impeller::StrokePathSegmentReceiver Class Reference
Inheritance diagram for impeller::StrokePathSegmentReceiver:
impeller::PathAndArcSegmentReceiver impeller::PathTessellator::SegmentReceiver

Public Member Functions

 StrokePathSegmentReceiver (Tessellator &tessellator, PositionWriter &vtx_builder, const StrokeParameters &stroke, const Scalar scale)
 

Protected Member Functions

void BeginContour (Point origin, bool will_be_closed) override
 
void RecordLine (Point p1, Point p2) override
 
void RecordQuad (Point p1, Point cp, Point p2) override
 
void RecordConic (Point p1, Point cp, Point p2, Scalar weight) override
 
void RecordCubic (Point p1, Point cp1, Point cp2, Point p2) override
 
template<typename Curve >
void RecordCurve (const Curve &curve)
 
void RecordCurveSegment (const SeparatedVector2 &prev_perpendicular, const Point cur, const SeparatedVector2 &cur_perpendicular)
 
void EndContour (Point origin, bool with_close) override
 
void RecordArc (const Arc &arc, const Point center, const Size radii) override
 

Detailed Description

StrokePathSegmentReceiver converts path segments (fed by PathTessellator) into a vertex strip that covers the outline of the stroked version of the path and feeds those vertices, expressed in the form of a vertex strip into the supplied PositionWriter.

The general procedure follows the following basic methodology:

Every path segment is represented by a box with two starting vertices perpendicular to its start point and two vertices perpendicular to its end point, all perpendiculars of length (stroke_width * 0.5).

Joins will connect the ending "box" perpendiculars of the previous segment to the starting "box" perpendiculars of the following segment. If the two boxes are so aligned that their adjacent perpendiculars are less than a threshold distance apart (kJoinPixelThreshold), the join will just be elided so that the end of one box becomes the start of the next box. If the join process does add decorations, it assumes that the ending perpendicular vertices from the prior segment are the last vertices added and ensures that it appends the two vertices for the starting perpendiculars of the new segment's "box". Thus every join either adds nothing and the end perpendiculars of the previous segment become the start perpendiculars of the next segment, or it makes sure its geometry fills in the gap and ends with the start perpendiculars for the new segment.

Prior to the start of an unclosed contour we insert a cap and also the starting perpendicular segments for the first segment. Prior to the start of a closed contour, we just insert the starting perpendiculars for the first segment. Either way, we've initialized the path with the starting perpendiculars of the first segment.

After the last segment in an unclosed contour we insert a cap which can assume that the last segment has already inserted its closing perpendicular segments. After the last segment in a closed contour, we insert a join back to the very first segment in that contour.

Connecting any two contours we insert an infinitely thin connecting thread by inserting the last point of the previous contour twice and then inserting the first point of the next contour twice. This ensures that there are no non-empty triangles between the two contours.

Finally, inserting a line segment can assume that the starting perpendiculars have already been inserted by the preceding cap, join, or prior segment, so all it needs to do is to insert the ending perpendiculars which set the process up for the subsequent cap, join, or future segment.

Inserting curve segments acts like a series of line segments except that the opening perpendicular is taken from the curve rather than the direction between the starting point and the first sample point. This ensures that any cap or join will be aligned with the curve and not tilted by the first approximating segment. The same is true of the ending perpendicular which is taken from the curve and not the last approximated segment. Between each approximated segment of the curve, we insert only Cap::kRound joins so as not to polygonize a curve when it turns very sharply. We also skip these joins for any change of direction which is smaller than the first sample point of a round join for performance reasons.

To facilitate all of that work we maintain variables containing SeparatedVector2 values that, by convention, point 90 degrees to the right of the given path direction. This facilitates a quick add/subtract from the point on the path to insert the necessary perpendicular points of a segment's box. These values contain both a unit vector for direction and a magnitude for length.

SeparatedVector2 values also allow us to quickly test limits on when to include joins by using a simple dot product on the previous and next perpendiculars at a given path point which should match the dot product of the path's direction itself at the same point since both perpendiculars have been rotated identically to the same side of the path. The SeparatedVector2 will perform the dot product on the unit-length vectors so that the result is exactly the cosine of the angle between the segments - also the angle by which the path turned at a given path point.

See also
PathTessellator::PathToStrokedSegments

Definition at line 131 of file stroke_path_geometry.cc.

Constructor & Destructor Documentation

◆ StrokePathSegmentReceiver()

impeller::StrokePathSegmentReceiver::StrokePathSegmentReceiver ( Tessellator tessellator,
PositionWriter &  vtx_builder,
const StrokeParameters stroke,
const Scalar  scale 
)
inline

Definition at line 133 of file stroke_path_geometry.cc.

137  : tessellator_(tessellator),
138  vtx_builder_(vtx_builder),
139  half_stroke_width_(stroke.width * 0.5f),
140  maximum_join_cosine_(
141  ComputeMaximumJoinCosine(scale, half_stroke_width_)),
142  minimum_miter_cosine_(ComputeMinimumMiterCosine(stroke.miter_limit)),
143  join_(stroke.join),
144  cap_(stroke.cap),
145  scale_(scale),
146  trigs_(MakeTrigs(tessellator, scale, half_stroke_width_)) {
147  // Trigs ensures that it always contains at least 2 entries.
148  FML_DCHECK(trigs_.size() >= 2);
149  FML_DCHECK(trigs_[0].cos == 1.0f); // Angle == 0 degrees
150  FML_DCHECK(trigs_[0].sin == 0.0f);
151  FML_DCHECK(trigs_.end()[-1].cos == 0.0f); // Angle == 90 degrees
152  FML_DCHECK(trigs_.end()[-1].sin == 1.0f);
153  }
std::vector< Trig >::iterator end() const
Definition: tessellator.h:55

References impeller::Tessellator::Trigs::end(), and impeller::Tessellator::Trigs::size().

Member Function Documentation

◆ BeginContour()

void impeller::StrokePathSegmentReceiver::BeginContour ( Point  origin,
bool  will_be_closed 
)
inlineoverrideprotectedvirtual

Every set of path segments will be surrounded by a Begin/EndContour pair with the same origin point.

Implements impeller::PathTessellator::SegmentReceiver.

Definition at line 157 of file stroke_path_geometry.cc.

157  {
158  if (has_prior_contour_ && origin != last_point_) {
159  // We only append these extra points if we have had a prior contour.
160  vtx_builder_.AppendVertex(last_point_);
161  vtx_builder_.AppendVertex(last_point_);
162  vtx_builder_.AppendVertex(origin);
163  vtx_builder_.AppendVertex(origin);
164  }
165  has_prior_contour_ = true;
166  has_prior_segment_ = false;
167  contour_needs_cap_ = !will_be_closed;
168  last_point_ = origin;
169  origin_point_ = origin;
170  }

◆ EndContour()

void impeller::StrokePathSegmentReceiver::EndContour ( Point  origin,
bool  with_close 
)
inlineoverrideprotectedvirtual

Every set of path segments will be surrounded by a Begin/EndContour pair with the same origin point. The boolean indicates if the path was closed as the result of an explicit PathReceiver::Close invocation which tells a stroking sub-class whether to use end caps or a "join to first segment". Contours which are closed by a MoveTo will supply "false".

Implements impeller::PathTessellator::SegmentReceiver.

Definition at line 271 of file stroke_path_geometry.cc.

271  {
272  FML_DCHECK(origin == origin_point_);
273  if (!has_prior_segment_) {
274  // Empty contour, fill in an axis aligned "cap box" at the origin.
275  FML_DCHECK(last_point_ == origin);
276  // kButt wouldn't fill anything so it defers to kSquare by convention.
277  Cap cap = (cap_ == Cap::kButt) ? Cap::kSquare : cap_;
278  Vector2 perpendicular = {-half_stroke_width_, 0};
279  AddCap(cap, origin, perpendicular, true);
280  if (cap == Cap::kRound) {
281  // Only round caps need the perpendicular between them to connect.
282  AppendVertices(origin, perpendicular);
283  }
284  AddCap(cap, origin, perpendicular, false);
285  } else if (with_close) {
286  // Closed contour, join back to origin.
287  FML_DCHECK(origin == origin_point_);
288  FML_DCHECK(last_point_ == origin);
289  AddJoin(join_, origin, last_perpendicular_, origin_perpendicular_);
290 
291  last_perpendicular_ = origin_perpendicular_;
292  last_point_ = origin;
293  } else {
294  AddCap(cap_, last_point_, last_perpendicular_.GetVector(), false);
295  }
296  has_prior_segment_ = false;
297  }
Point Vector2
Definition: point.h:429
Cap
An enum that describes ways to decorate the end of a path contour.
Vector2 GetVector() const
Returns the vector representation of the vector.

References impeller::SeparatedVector2::GetVector(), impeller::kButt, impeller::kRound, and impeller::kSquare.

◆ RecordArc()

void impeller::StrokePathSegmentReceiver::RecordArc ( const Arc arc,
const Point  center,
const Size  radii 
)
inlineoverrideprotectedvirtual

Implements impeller::PathAndArcSegmentReceiver.

Definition at line 300 of file stroke_path_geometry.cc.

302  {
303  Tessellator::Trigs trigs =
304  tessellator_.GetTrigsForDeviceRadius(scale_ * radii.MaxDimension());
305  Arc::Iteration iterator = arc.ComputeIterations(trigs.GetSteps(), false);
306 
307  SeparatedVector2 prev_perpendicular =
308  PerpendicularFromUnitDirection({-iterator.start.y, iterator.start.x});
309  HandlePreviousJoin(prev_perpendicular);
310 
311  for (size_t i = 0u; i < iterator.quadrant_count; i++) {
312  Arc::Iteration::Quadrant quadrant = iterator.quadrants[i];
313  for (size_t j = quadrant.start_index; j < quadrant.end_index; j++) {
314  Vector2 direction = trigs[j] * quadrant.axis;
315  Point cur = center + direction * radii;
316  SeparatedVector2 cur_perpendicular =
317  PerpendicularFromUnitDirection({-direction.y, direction.x});
318  RecordCurveSegment(prev_perpendicular, cur, cur_perpendicular);
319  prev_perpendicular = cur_perpendicular;
320  }
321  }
322 
323  SeparatedVector2 end_perpendicular =
324  PerpendicularFromUnitDirection({-iterator.end.y, iterator.end.x});
325  Point end = center + iterator.end * radii;
326  RecordCurveSegment(prev_perpendicular, end, end_perpendicular);
327 
328  last_perpendicular_ = end_perpendicular;
329  last_point_ = end;
330  }
void RecordCurveSegment(const SeparatedVector2 &prev_perpendicular, const Point cur, const SeparatedVector2 &cur_perpendicular)
Trigs GetTrigsForDeviceRadius(Scalar pixel_radius)
Definition: tessellator.cc:425
TPoint< Scalar > Point
Definition: point.h:425
const size_t end

References impeller::Arc::Iteration::Quadrant::axis, impeller::Arc::ComputeIterations(), impeller::Arc::Iteration::end, end, impeller::Arc::Iteration::Quadrant::end_index, impeller::Tessellator::Trigs::GetSteps(), impeller::Tessellator::GetTrigsForDeviceRadius(), impeller::TSize< T >::MaxDimension(), impeller::Arc::Iteration::quadrant_count, impeller::Arc::Iteration::quadrants, RecordCurveSegment(), impeller::Arc::Iteration::start, impeller::Arc::Iteration::Quadrant::start_index, impeller::TPoint< T >::x, and impeller::TPoint< T >::y.

◆ RecordConic()

void impeller::StrokePathSegmentReceiver::RecordConic ( Point  p1,
Point  cp,
Point  p2,
Scalar  weight 
)
inlineoverrideprotectedvirtual

Guaranteed to be non-degenerate (not a quad or line) p1 will always be the last recorded point.

Implements impeller::PathTessellator::SegmentReceiver.

Definition at line 191 of file stroke_path_geometry.cc.

191  {
192  RecordCurve<PathTessellator::Conic>({p1, cp, p2, weight});
193  }
Scalar weight

References p1, p2, and weight.

◆ RecordCubic()

void impeller::StrokePathSegmentReceiver::RecordCubic ( Point  p1,
Point  cp1,
Point  cp2,
Point  p2 
)
inlineoverrideprotectedvirtual

Guaranteed to be trivially non-degenerate (not all 4 points the same). p1 will always be the last recorded point.

Implements impeller::PathTessellator::SegmentReceiver.

Definition at line 196 of file stroke_path_geometry.cc.

196  {
197  RecordCurve<PathTessellator::Cubic>({p1, cp1, cp2, p2});
198  }

References p1, and p2.

◆ RecordCurve()

template<typename Curve >
void impeller::StrokePathSegmentReceiver::RecordCurve ( const Curve &  curve)
inlineprotected

Definition at line 202 of file stroke_path_geometry.cc.

202  {
203  std::optional<Point> start_direction = curve.GetStartDirection();
204  std::optional<Point> end_direction = curve.GetEndDirection();
205 
206  // The Prune receiver should have eliminated any empty curves, so any
207  // curve we see should have both start and end direction.
208  FML_DCHECK(start_direction.has_value() && end_direction.has_value());
209 
210  // In order to keep the compiler/lint happy we check for values anyway.
211  if (start_direction.has_value() && end_direction.has_value()) {
212  // We now know the curve cannot be degenerate.
213  SeparatedVector2 start_perpendicular =
214  PerpendicularFromUnitDirection(-start_direction.value());
215  SeparatedVector2 end_perpendicular =
216  PerpendicularFromUnitDirection(end_direction.value());
217 
218  // We join the previous segment to this one with a normal join
219  // The join will append the perpendicular at the start of this
220  // curve as well.
221  HandlePreviousJoin(start_perpendicular);
222 
223  // We use the scale suggested by the transform basis which is the
224  // same scale that would be used for filling the path. But we also
225  // need to adjust the scale for the magnification of the curve
226  // features that occurs when we draw with a wide pen and the outer
227  // curves of that stroked path are larger than the base curve itself.
228  // So, we scale by both the transform basis and (half) the stroke
229  // width, but we also make sure that we aren't reducing the scale
230  // in the uncommon case that someone is drawing at a large scale
231  // with a very tiny stroke width. To accomplish this, we multiply
232  // the scale basis by half the stroke width, but make sure the width
233  // is at least 1.0 so that we don't reduce the natural transform scale.
234  Scalar stroke_scale = scale_ * std::max(1.0f, half_stroke_width_);
235  Scalar count = std::ceilf(curve.SubdivisionCount(stroke_scale));
236 
237  Point prev = curve.p1;
238  SeparatedVector2 prev_perpendicular = start_perpendicular;
239 
240  // Handle all intermediate curve points up to but not including the end.
241  for (int i = 1; i < count; i++) {
242  Point cur = curve.Solve(i / count);
243  SeparatedVector2 cur_perpendicular = PerpendicularFromPoints(prev, cur);
244  RecordCurveSegment(prev_perpendicular, cur, cur_perpendicular);
245  prev = cur;
246  prev_perpendicular = cur_perpendicular;
247  }
248 
249  RecordCurveSegment(prev_perpendicular, curve.p2, end_perpendicular);
250 
251  last_perpendicular_ = end_perpendicular;
252  last_point_ = curve.p2;
253  }
254  }
float Scalar
Definition: scalar.h:19

References RecordCurveSegment().

◆ RecordCurveSegment()

void impeller::StrokePathSegmentReceiver::RecordCurveSegment ( const SeparatedVector2 prev_perpendicular,
const Point  cur,
const SeparatedVector2 cur_perpendicular 
)
inlineprotected

Definition at line 256 of file stroke_path_geometry.cc.

258  {
259  if (prev_perpendicular.GetAlignment(cur_perpendicular) < trigs_[1].cos) {
260  // We only connect 2 curved segments if their change in direction
261  // is faster than a single sample of a round join. We always use a
262  // round join here because this is about smoothness of curves rather
263  // than a decoration for specific segments of the path.
264  AppendVertices(cur, prev_perpendicular);
265  AddJoin(Join::kRound, cur, prev_perpendicular, cur_perpendicular);
266  }
267  AppendVertices(cur, cur_perpendicular);
268  }

References impeller::SeparatedVector2::GetAlignment(), and impeller::kRound.

Referenced by RecordArc(), and RecordCurve().

◆ RecordLine()

void impeller::StrokePathSegmentReceiver::RecordLine ( Point  p1,
Point  p2 
)
inlineoverrideprotectedvirtual

Guaranteed to be non-degenerate except in the single case of stroking where we have a MoveTo followed by any number of degenerate (single point, going nowhere) path segments. p1 will always be the last recorded point.

Implements impeller::PathTessellator::SegmentReceiver.

Definition at line 173 of file stroke_path_geometry.cc.

173  {
174  if (p2 != p1) {
175  SeparatedVector2 current_perpendicular = PerpendicularFromPoints(p1, p2);
176 
177  HandlePreviousJoin(current_perpendicular);
178  AppendVertices(p2, current_perpendicular);
179 
180  last_perpendicular_ = current_perpendicular;
181  last_point_ = p2;
182  }
183  }

References p1, and p2.

◆ RecordQuad()

void impeller::StrokePathSegmentReceiver::RecordQuad ( Point  p1,
Point  cp,
Point  p2 
)
inlineoverrideprotectedvirtual

Guaranteed to be non-degenerate (not a line). p1 will always be the last recorded point.

Implements impeller::PathTessellator::SegmentReceiver.

Definition at line 186 of file stroke_path_geometry.cc.

186  {
187  RecordCurve<PathTessellator::Quad>({p1, cp, p2});
188  }

References p1, and p2.


The documentation for this class was generated from the following file: