1 /* 2 * Copyright 2015 Google Inc. 3 * 4 * Use of this source code is governed by a BSD-style license that can be 5 * found in the LICENSE file. 6 */ 7 8 #ifndef GrTriangulator_DEFINED 9 #define GrTriangulator_DEFINED 10 11 #if !defined(SK_ENABLE_OPTIMIZE_SIZE) 12 13 #include "include/core/SkPath.h" 14 #include "include/core/SkPoint.h" 15 #include "include/core/SkScalar.h" 16 #include "include/private/base/SkAssert.h" 17 #include "src/base/SkArenaAlloc.h" 18 #include "src/gpu/BufferWriter.h" 19 20 #include <algorithm> 21 #include <cmath> 22 #include <cstdint> 23 #include <tuple> 24 25 class GrEagerVertexAllocator; 26 enum class SkPathFillType; 27 struct SkRect; 28 29 #define TRIANGULATOR_LOGGING 0 30 #define TRIANGULATOR_WIREFRAME 0 31 32 /** 33 * Provides utility functions for converting paths to a collection of triangles. 34 */ 35 class GrTriangulator { 36 public: 37 constexpr static int kArenaDefaultChunkSize = 16 * 1024; 38 PathToTriangles(const SkPath & path,SkScalar tolerance,const SkRect & clipBounds,GrEagerVertexAllocator * vertexAllocator,bool * isLinear)39 static int PathToTriangles(const SkPath& path, SkScalar tolerance, const SkRect& clipBounds, 40 GrEagerVertexAllocator* vertexAllocator, bool* isLinear) { 41 if (!path.isFinite()) { 42 return 0; 43 } 44 SkArenaAlloc alloc(kArenaDefaultChunkSize); 45 GrTriangulator triangulator(path, &alloc); 46 auto [ polys, success ] = triangulator.pathToPolys(tolerance, clipBounds, isLinear); 47 if (!success) { 48 return 0; 49 } 50 int count = triangulator.polysToTriangles(polys, vertexAllocator); 51 return count; 52 } 53 54 // Enums used by GrTriangulator internals. 55 enum class Side { kLeft, kRight }; 56 enum class EdgeType { kInner, kOuter, kConnector }; 57 58 // Structs used by GrTriangulator internals. 59 struct Vertex; 60 struct VertexList; 61 struct Line; 62 struct Edge; 63 struct EdgeList; 64 struct MonotonePoly; 65 struct Poly; 66 struct Comparator; 67 68 protected: GrTriangulator(const SkPath & path,SkArenaAlloc * alloc)69 GrTriangulator(const SkPath& path, SkArenaAlloc* alloc) : fPath(path), fAlloc(alloc) {} ~GrTriangulator()70 virtual ~GrTriangulator() {} 71 72 // There are six stages to the basic algorithm: 73 // 74 // 1) Linearize the path contours into piecewise linear segments: 75 void pathToContours(float tolerance, const SkRect& clipBounds, VertexList* contours, 76 bool* isLinear) const; 77 78 // 2) Build a mesh of edges connecting the vertices: 79 void contoursToMesh(VertexList* contours, int contourCnt, VertexList* mesh, 80 const Comparator&); 81 82 // 3) Sort the vertices in Y (and secondarily in X): 83 static void SortedMerge(VertexList* front, VertexList* back, VertexList* result, 84 const Comparator&); 85 static void SortMesh(VertexList* vertices, const Comparator&); 86 87 // 4) Simplify the mesh by inserting new vertices at intersecting edges: 88 enum class SimplifyResult { 89 kFailed, 90 kAlreadySimple, 91 kFoundSelfIntersection 92 }; 93 94 enum class BoolFail { 95 kFalse, 96 kTrue, 97 kFail 98 }; 99 100 [[nodiscard]] SimplifyResult simplify(VertexList* mesh, const Comparator&); 101 102 // 5) Tessellate the simplified mesh into monotone polygons: 103 virtual std::tuple<Poly*, bool> tessellate(const VertexList& vertices, const Comparator&); 104 105 // 6) Triangulate the monotone polygons directly into a vertex buffer: 106 skgpu::VertexWriter polysToTriangles(Poly* polys, 107 SkPathFillType overrideFillType, 108 skgpu::VertexWriter data) const; 109 110 // The vertex sorting in step (3) is a merge sort, since it plays well with the linked list 111 // of vertices (and the necessity of inserting new vertices on intersection). 112 // 113 // Stages (4) and (5) use an active edge list -- a list of all edges for which the 114 // sweep line has crossed the top vertex, but not the bottom vertex. It's sorted 115 // left-to-right based on the point where both edges are active (when both top vertices 116 // have been seen, so the "lower" top vertex of the two). If the top vertices are equal 117 // (shared), it's sorted based on the last point where both edges are active, so the 118 // "upper" bottom vertex. 119 // 120 // The most complex step is the simplification (4). It's based on the Bentley-Ottman 121 // line-sweep algorithm, but due to floating point inaccuracy, the intersection points are 122 // not exact and may violate the mesh topology or active edge list ordering. We 123 // accommodate this by adjusting the topology of the mesh and AEL to match the intersection 124 // points. This occurs in two ways: 125 // 126 // A) Intersections may cause a shortened edge to no longer be ordered with respect to its 127 // neighbouring edges at the top or bottom vertex. This is handled by merging the 128 // edges (mergeCollinearVertices()). 129 // B) Intersections may cause an edge to violate the left-to-right ordering of the 130 // active edge list. This is handled by detecting potential violations and rewinding 131 // the active edge list to the vertex before they occur (rewind() during merging, 132 // rewind_if_necessary() during splitting). 133 // 134 // The tessellation steps (5) and (6) are based on "Triangulating Simple Polygons and 135 // Equivalent Problems" (Fournier and Montuno); also a line-sweep algorithm. Note that it 136 // currently uses a linked list for the active edge list, rather than a 2-3 tree as the 137 // paper describes. The 2-3 tree gives O(lg N) lookups, but insertion and removal also 138 // become O(lg N). In all the test cases, it was found that the cost of frequent O(lg N) 139 // insertions and removals was greater than the cost of infrequent O(N) lookups with the 140 // linked list implementation. With the latter, all removals are O(1), and most insertions 141 // are O(1), since we know the adjacent edge in the active edge list based on the topology. 142 // Only type 2 vertices (see paper) require the O(N) lookups, and these are much less 143 // frequent. There may be other data structures worth investigating, however. 144 // 145 // Note that the orientation of the line sweep algorithms is determined by the aspect ratio of 146 // the path bounds. When the path is taller than it is wide, we sort vertices based on 147 // increasing Y coordinate, and secondarily by increasing X coordinate. When the path is wider 148 // than it is tall, we sort by increasing X coordinate, but secondarily by *decreasing* Y 149 // coordinate. This is so that the "left" and "right" orientation in the code remains correct 150 // (edges to the left are increasing in Y; edges to the right are decreasing in Y). That is, the 151 // setting rotates 90 degrees counterclockwise, rather that transposing. 152 153 // Additional helpers and driver functions. 154 skgpu::VertexWriter emitMonotonePoly(const MonotonePoly*, skgpu::VertexWriter data) const; 155 skgpu::VertexWriter emitTriangle(Vertex* prev, Vertex* curr, Vertex* next, int winding, 156 skgpu::VertexWriter data) const; 157 skgpu::VertexWriter emitPoly(const Poly*, skgpu::VertexWriter data) const; 158 159 Poly* makePoly(Poly** head, Vertex* v, int winding) const; 160 void appendPointToContour(const SkPoint& p, VertexList* contour) const; 161 void appendQuadraticToContour(const SkPoint[3], SkScalar toleranceSqd, 162 VertexList* contour) const; 163 void generateCubicPoints(const SkPoint&, const SkPoint&, const SkPoint&, const SkPoint&, 164 SkScalar tolSqd, VertexList* contour, int pointsLeft) const; 165 bool applyFillType(int winding) const; 166 MonotonePoly* allocateMonotonePoly(Edge* edge, Side side, int winding); 167 Edge* allocateEdge(Vertex* top, Vertex* bottom, int winding, EdgeType type); 168 Edge* makeEdge(Vertex* prev, Vertex* next, EdgeType type, const Comparator&); 169 [[nodiscard]] bool setTop( 170 Edge* edge, Vertex* v, EdgeList* activeEdges, Vertex** current, const Comparator&) const; 171 [[nodiscard]] bool setBottom( 172 Edge* edge, Vertex* v, EdgeList* activeEdges, Vertex** current, const Comparator&) const; 173 [[nodiscard]] bool mergeEdgesAbove( 174 Edge* edge, Edge* other, EdgeList* activeEdges, Vertex** current, const Comparator&) const; 175 [[nodiscard]] bool mergeEdgesBelow( 176 Edge* edge, Edge* other, EdgeList* activeEdges, Vertex** current, const Comparator&) const; 177 Edge* makeConnectingEdge(Vertex* prev, Vertex* next, EdgeType, const Comparator&, 178 int windingScale = 1); 179 void mergeVertices(Vertex* src, Vertex* dst, VertexList* mesh, const Comparator&) const; 180 static void FindEnclosingEdges(const Vertex& v, const EdgeList& edges, 181 Edge** left, Edge** right); 182 bool mergeCollinearEdges(Edge* edge, EdgeList* activeEdges, Vertex** current, 183 const Comparator&) const; 184 BoolFail splitEdge(Edge* edge, Vertex* v, EdgeList* activeEdges, Vertex** current, 185 const Comparator&); 186 BoolFail intersectEdgePair(Edge* left, Edge* right, EdgeList* activeEdges, Vertex** current, 187 const Comparator&); 188 Vertex* makeSortedVertex(const SkPoint&, uint8_t alpha, VertexList* mesh, Vertex* reference, 189 const Comparator&) const; 190 void computeBisector(Edge* edge1, Edge* edge2, Vertex*) const; 191 BoolFail checkForIntersection(Edge* left, Edge* right, EdgeList* activeEdges, Vertex** current, 192 VertexList* mesh, const Comparator&); 193 void sanitizeContours(VertexList* contours, int contourCnt) const; 194 bool mergeCoincidentVertices(VertexList* mesh, const Comparator&) const; 195 void buildEdges(VertexList* contours, int contourCnt, VertexList* mesh, 196 const Comparator&); 197 std::tuple<Poly*, bool> contoursToPolys(VertexList* contours, int contourCnt); 198 std::tuple<Poly*, bool> pathToPolys(float tolerance, const SkRect& clipBounds, 199 bool* isLinear); 200 static int64_t CountPoints(Poly* polys, SkPathFillType overrideFillType); 201 int polysToTriangles(Poly*, GrEagerVertexAllocator*) const; 202 203 // FIXME: fPath should be plumbed through function parameters instead. 204 const SkPath fPath; 205 SkArenaAlloc* const fAlloc; 206 int fNumMonotonePolys = 0; 207 int fNumEdges = 0; 208 209 // Internal control knobs. 210 bool fRoundVerticesToQuarterPixel = false; 211 bool fEmitCoverage = false; 212 bool fPreserveCollinearVertices = false; 213 bool fCollectBreadcrumbTriangles = false; 214 215 // The breadcrumb triangles serve as a glue that erases T-junctions between a path's outer 216 // curves and its inner polygon triangulation. Drawing a path's outer curves, breadcrumb 217 // triangles, and inner polygon triangulation all together into the stencil buffer has the same 218 // identical rasterized effect as stenciling a classic Redbook fan. 219 // 220 // The breadcrumb triangles track all the edge splits that led from the original inner polygon 221 // edges to the final triangulation. Every time an edge splits, we emit a razor-thin breadcrumb 222 // triangle consisting of the edge's original endpoints and the split point. (We also add 223 // supplemental breadcrumb triangles to areas where abs(winding) > 1.) 224 // 225 // a 226 // / 227 // / 228 // / 229 // x <- Edge splits at x. New breadcrumb triangle is: [a, b, x]. 230 // / 231 // / 232 // b 233 // 234 // The opposite-direction shared edges between the triangulation and breadcrumb triangles should 235 // all cancel out, leaving just the set of edges from the original polygon. 236 class BreadcrumbTriangleList { 237 public: 238 struct Node { NodeNode239 Node(SkPoint a, SkPoint b, SkPoint c) : fPts{a, b, c} {} 240 SkPoint fPts[3]; 241 Node* fNext = nullptr; 242 }; head()243 const Node* head() const { return fHead; } count()244 int count() const { return fCount; } 245 append(SkArenaAlloc * alloc,SkPoint a,SkPoint b,SkPoint c,int winding)246 void append(SkArenaAlloc* alloc, SkPoint a, SkPoint b, SkPoint c, int winding) { 247 if (a == b || a == c || b == c || winding == 0) { 248 return; 249 } 250 if (winding < 0) { 251 std::swap(a, b); 252 winding = -winding; 253 } 254 for (int i = 0; i < winding; ++i) { 255 SkASSERT(fTail && !(*fTail)); 256 *fTail = alloc->make<Node>(a, b, c); 257 fTail = &(*fTail)->fNext; 258 } 259 fCount += winding; 260 } 261 concat(BreadcrumbTriangleList && list)262 void concat(BreadcrumbTriangleList&& list) { 263 SkASSERT(fTail && !(*fTail)); 264 if (list.fHead) { 265 *fTail = list.fHead; 266 fTail = list.fTail; 267 fCount += list.fCount; 268 list.fHead = nullptr; 269 list.fTail = &list.fHead; 270 list.fCount = 0; 271 } 272 } 273 274 private: 275 Node* fHead = nullptr; 276 Node** fTail = &fHead; 277 int fCount = 0; 278 }; 279 280 mutable BreadcrumbTriangleList fBreadcrumbList; 281 }; 282 283 /** 284 * Vertices are used in three ways: first, the path contours are converted into a 285 * circularly-linked list of Vertices for each contour. After edge construction, the same Vertices 286 * are re-ordered by the merge sort according to the sweep_lt comparator (usually, increasing 287 * in Y) using the same fPrev/fNext pointers that were used for the contours, to avoid 288 * reallocation. Finally, MonotonePolys are built containing a circularly-linked list of 289 * Vertices. (Currently, those Vertices are newly-allocated for the MonotonePolys, since 290 * an individual Vertex from the path mesh may belong to multiple 291 * MonotonePolys, so the original Vertices cannot be re-used. 292 */ 293 294 struct GrTriangulator::Vertex { VertexVertex295 Vertex(const SkPoint& point, uint8_t alpha) 296 : fPoint(point), fPrev(nullptr), fNext(nullptr) 297 , fFirstEdgeAbove(nullptr), fLastEdgeAbove(nullptr) 298 , fFirstEdgeBelow(nullptr), fLastEdgeBelow(nullptr) 299 , fLeftEnclosingEdge(nullptr), fRightEnclosingEdge(nullptr) 300 , fPartner(nullptr) 301 , fAlpha(alpha) 302 , fSynthetic(false) 303 #if TRIANGULATOR_LOGGING 304 , fID (-1.0f) 305 #endif 306 {} 307 SkPoint fPoint; // Vertex position 308 Vertex* fPrev; // Linked list of contours, then Y-sorted vertices. 309 Vertex* fNext; // " 310 Edge* fFirstEdgeAbove; // Linked list of edges above this vertex. 311 Edge* fLastEdgeAbove; // " 312 Edge* fFirstEdgeBelow; // Linked list of edges below this vertex. 313 Edge* fLastEdgeBelow; // " 314 Edge* fLeftEnclosingEdge; // Nearest edge in the AEL left of this vertex. 315 Edge* fRightEnclosingEdge; // Nearest edge in the AEL right of this vertex. 316 Vertex* fPartner; // Corresponding inner or outer vertex (for AA). 317 uint8_t fAlpha; 318 bool fSynthetic; // Is this a synthetic vertex? 319 #if TRIANGULATOR_LOGGING 320 float fID; // Identifier used for logging. 321 #endif isConnectedVertex322 bool isConnected() const { return this->fFirstEdgeAbove || this->fFirstEdgeBelow; } 323 }; 324 325 struct GrTriangulator::VertexList { VertexListVertexList326 VertexList() : fHead(nullptr), fTail(nullptr) {} VertexListVertexList327 VertexList(Vertex* head, Vertex* tail) : fHead(head), fTail(tail) {} 328 Vertex* fHead; 329 Vertex* fTail; 330 void insert(Vertex* v, Vertex* prev, Vertex* next); appendVertexList331 void append(Vertex* v) { insert(v, fTail, nullptr); } appendVertexList332 void append(const VertexList& list) { 333 if (!list.fHead) { 334 return; 335 } 336 if (fTail) { 337 fTail->fNext = list.fHead; 338 list.fHead->fPrev = fTail; 339 } else { 340 fHead = list.fHead; 341 } 342 fTail = list.fTail; 343 } prependVertexList344 void prepend(Vertex* v) { insert(v, nullptr, fHead); } 345 void remove(Vertex* v); closeVertexList346 void close() { 347 if (fHead && fTail) { 348 fTail->fNext = fHead; 349 fHead->fPrev = fTail; 350 } 351 } 352 #if TRIANGULATOR_LOGGING 353 void dump() const; 354 #endif 355 }; 356 357 // A line equation in implicit form. fA * x + fB * y + fC = 0, for all points (x, y) on the line. 358 struct GrTriangulator::Line { LineLine359 Line(double a, double b, double c) : fA(a), fB(b), fC(c) {} LineLine360 Line(Vertex* p, Vertex* q) : Line(p->fPoint, q->fPoint) {} LineLine361 Line(const SkPoint& p, const SkPoint& q) 362 : fA(static_cast<double>(q.fY) - p.fY) // a = dY 363 , fB(static_cast<double>(p.fX) - q.fX) // b = -dX 364 , fC(static_cast<double>(p.fY) * q.fX - // c = cross(q, p) 365 static_cast<double>(p.fX) * q.fY) {} distLine366 double dist(const SkPoint& p) const { return fA * p.fX + fB * p.fY + fC; } 367 Line operator*(double v) const { return Line(fA * v, fB * v, fC * v); } magSqLine368 double magSq() const { return fA * fA + fB * fB; } normalizeLine369 void normalize() { 370 double len = sqrt(this->magSq()); 371 if (len == 0.0) { 372 return; 373 } 374 double scale = 1.0f / len; 375 fA *= scale; 376 fB *= scale; 377 fC *= scale; 378 } nearParallelLine379 bool nearParallel(const Line& o) const { 380 return fabs(o.fA - fA) < 0.00001 && fabs(o.fB - fB) < 0.00001; 381 } 382 383 // Compute the intersection of two (infinite) Lines. 384 bool intersect(const Line& other, SkPoint* point) const; 385 double fA, fB, fC; 386 }; 387 388 /** 389 * An Edge joins a top Vertex to a bottom Vertex. Edge ordering for the list of "edges above" and 390 * "edge below" a vertex as well as for the active edge list is handled by isLeftOf()/isRightOf(). 391 * Note that an Edge will give occasionally dist() != 0 for its own endpoints (because floating 392 * point). For speed, that case is only tested by the callers that require it (e.g., 393 * rewind_if_necessary()). Edges also handle checking for intersection with other edges. 394 * Currently, this converts the edges to the parametric form, in order to avoid doing a division 395 * until an intersection has been confirmed. This is slightly slower in the "found" case, but 396 * a lot faster in the "not found" case. 397 * 398 * The coefficients of the line equation stored in double precision to avoid catastrophic 399 * cancellation in the isLeftOf() and isRightOf() checks. Using doubles ensures that the result is 400 * correct in float, since it's a polynomial of degree 2. The intersect() function, being 401 * degree 5, is still subject to catastrophic cancellation. We deal with that by assuming its 402 * output may be incorrect, and adjusting the mesh topology to match (see comment at the top of 403 * this file). 404 */ 405 406 struct GrTriangulator::Edge { EdgeEdge407 Edge(Vertex* top, Vertex* bottom, int winding, EdgeType type) 408 : fWinding(winding) 409 , fTop(top) 410 , fBottom(bottom) 411 , fType(type) 412 , fLeft(nullptr) 413 , fRight(nullptr) 414 , fPrevEdgeAbove(nullptr) 415 , fNextEdgeAbove(nullptr) 416 , fPrevEdgeBelow(nullptr) 417 , fNextEdgeBelow(nullptr) 418 , fLeftPoly(nullptr) 419 , fRightPoly(nullptr) 420 , fLeftPolyPrev(nullptr) 421 , fLeftPolyNext(nullptr) 422 , fRightPolyPrev(nullptr) 423 , fRightPolyNext(nullptr) 424 , fUsedInLeftPoly(false) 425 , fUsedInRightPoly(false) 426 , fLine(top, bottom) { 427 } 428 int fWinding; // 1 == edge goes downward; -1 = edge goes upward. 429 Vertex* fTop; // The top vertex in vertex-sort-order (sweep_lt). 430 Vertex* fBottom; // The bottom vertex in vertex-sort-order. 431 EdgeType fType; 432 Edge* fLeft; // The linked list of edges in the active edge list. 433 Edge* fRight; // " 434 Edge* fPrevEdgeAbove; // The linked list of edges in the bottom Vertex's "edges above". 435 Edge* fNextEdgeAbove; // " 436 Edge* fPrevEdgeBelow; // The linked list of edges in the top Vertex's "edges below". 437 Edge* fNextEdgeBelow; // " 438 Poly* fLeftPoly; // The Poly to the left of this edge, if any. 439 Poly* fRightPoly; // The Poly to the right of this edge, if any. 440 Edge* fLeftPolyPrev; 441 Edge* fLeftPolyNext; 442 Edge* fRightPolyPrev; 443 Edge* fRightPolyNext; 444 bool fUsedInLeftPoly; 445 bool fUsedInRightPoly; 446 Line fLine; 447 distEdge448 double dist(const SkPoint& p) const { 449 // Coerce points coincident with the vertices to have dist = 0, since converting from 450 // a double intersection point back to float storage might construct a point that's no 451 // longer on the ideal line. 452 return (p == fTop->fPoint || p == fBottom->fPoint) ? 0.0 : fLine.dist(p); 453 } isRightOfEdge454 bool isRightOf(const Vertex& v) const { return this->dist(v.fPoint) < 0.0; } isLeftOfEdge455 bool isLeftOf(const Vertex& v) const { return this->dist(v.fPoint) > 0.0; } recomputeEdge456 void recompute() { fLine = Line(fTop, fBottom); } 457 void insertAbove(Vertex*, const Comparator&); 458 void insertBelow(Vertex*, const Comparator&); 459 void disconnect(); 460 bool intersect(const Edge& other, SkPoint* p, uint8_t* alpha = nullptr) const; 461 }; 462 463 struct GrTriangulator::EdgeList { EdgeListEdgeList464 EdgeList() : fHead(nullptr), fTail(nullptr) {} 465 Edge* fHead; 466 Edge* fTail; 467 void insert(Edge* edge, Edge* prev, Edge* next); 468 bool insert(Edge* edge, Edge* prev); appendEdgeList469 void append(Edge* e) { insert(e, fTail, nullptr); } 470 bool remove(Edge* edge); removeAllEdgeList471 void removeAll() { 472 while (fHead) { 473 this->remove(fHead); 474 } 475 } closeEdgeList476 void close() { 477 if (fHead && fTail) { 478 fTail->fRight = fHead; 479 fHead->fLeft = fTail; 480 } 481 } containsEdgeList482 bool contains(Edge* edge) const { return edge->fLeft || edge->fRight || fHead == edge; } 483 }; 484 485 struct GrTriangulator::MonotonePoly { MonotonePolyMonotonePoly486 MonotonePoly(Edge* edge, Side side, int winding) 487 : fSide(side) 488 , fFirstEdge(nullptr) 489 , fLastEdge(nullptr) 490 , fPrev(nullptr) 491 , fNext(nullptr) 492 , fWinding(winding) { 493 this->addEdge(edge); 494 } 495 Side fSide; 496 Edge* fFirstEdge; 497 Edge* fLastEdge; 498 MonotonePoly* fPrev; 499 MonotonePoly* fNext; 500 int fWinding; 501 void addEdge(Edge*); 502 }; 503 504 struct GrTriangulator::Poly { 505 Poly(Vertex* v, int winding); 506 507 Poly* addEdge(Edge* e, Side side, GrTriangulator*); lastVertexPoly508 Vertex* lastVertex() const { return fTail ? fTail->fLastEdge->fBottom : fFirstVertex; } 509 Vertex* fFirstVertex; 510 int fWinding; 511 MonotonePoly* fHead; 512 MonotonePoly* fTail; 513 Poly* fNext; 514 Poly* fPartner; 515 int fCount; 516 #if TRIANGULATOR_LOGGING 517 int fID; 518 #endif 519 }; 520 521 struct GrTriangulator::Comparator { 522 enum class Direction { kVertical, kHorizontal }; ComparatorComparator523 Comparator(Direction direction) : fDirection(direction) {} 524 bool sweep_lt(const SkPoint& a, const SkPoint& b) const; 525 Direction fDirection; 526 }; 527 528 #endif // SK_ENABLE_OPTIMIZE_SIZE 529 530 #endif // GrTriangulator_DEFINED 531