├── .gitignore ├── CMakeLists.txt ├── LICENSE.md ├── README.md └── include ├── LineSegment.h ├── Vec2.h └── Polyline2D.h /.gitignore: -------------------------------------------------------------------------------- 1 | # JetBrains IDEs 2 | .idea 3 | cmake-build-debug 4 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.9) 2 | project(Polyline2D VERSION 0.0.1) 3 | 4 | include(GNUInstallDirs) 5 | 6 | add_library(${PROJECT_NAME} INTERFACE) 7 | 8 | target_include_directories(${PROJECT_NAME} INTERFACE include/) 9 | 10 | install(DIRECTORY include/ 11 | DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}) 12 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright © 2019 Marius Metzger (CrushedPixel) 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the “Software”), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. 26 | 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Polyline2D 2 | *Polyline2D* is a header-only C++17 library that generates a triangle mesh from a list of points. 3 | It can be used to draw thick lines with rendering APIs like *OpenGL*, which do not support line drawing out of the box. 4 | It supports common joint and end cap styles. 5 | 6 | ## Example usage 7 | ```c++ 8 | #include 9 | using namespace crushedpixel; 10 | 11 | std::vector points{ 12 | { -0.25f, -0.5f }, 13 | { -0.25f, 0.5f }, 14 | { 0.25f, 0.25f }, 15 | { 0.0f, 0.0f }, 16 | { 0.25f, -0.25f }, 17 | { -0.4f, -0.25f } 18 | }; 19 | 20 | auto thickness = 0.1f; 21 | auto vertices = Polyline2D::create(points, thickness, 22 | Polyline2D::JointStyle::ROUND, 23 | Polyline2D::EndCapStyle::SQUARE); 24 | 25 | // render vertices, for example using OpenGL... 26 | ``` 27 | This code results in the following mesh: 28 | ![Result](https://i.imgur.com/D0lvyYT.png) 29 | For demonstration purposes, the generated mesh is once rendered in wireframe mode (light green), and once in fill mode (transparent green). 30 | 31 | The red points show the input points. 32 | 33 | There is *no overdraw* within segments, only lines that overlap are filled twice. 34 | 35 | For an example application using this software, visit [Polyline2DExample](https://github.com/CrushedPixel/Polyline2DExample). 36 | 37 | ## Installation 38 | ### Manual 39 | To use *Polyline2D*, simply clone this repository and include the file `Polyline2D.h` from the `include` directory. 40 | 41 | ### Using CMake 42 | To install the header files into your global header directory, you can use CMake: 43 | ``` 44 | mkdir build 45 | cd build 46 | cmake .. 47 | make install 48 | ``` 49 | 50 | You can then include the header file using `#include `. 51 | 52 | ## Attribution and similar projects 53 | The concepts and maths behind this project are largely inspired by [this amazing blog post](https://artgrammer.blogspot.com/2011/07/drawing-polylines-by-tessellation.html) by "To be an Artgrammer". 54 | 55 | Check out the original author's project 56 | [vaserenderer](https://github.com/tyt2y3/vaserenderer), which also offers 57 | a C# implementation based on fragment shaders. 58 | -------------------------------------------------------------------------------- /include/LineSegment.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Vec2.h" 4 | #include 5 | 6 | namespace crushedpixel { 7 | 8 | template 9 | struct LineSegment { 10 | LineSegment(const Vec2 &a, const Vec2 &b) : 11 | a(a), b(b) {} 12 | 13 | Vec2 a, b; 14 | 15 | /** 16 | * @return A copy of the line segment, offset by the given vector. 17 | */ 18 | LineSegment operator+(const Vec2 &toAdd) const { 19 | return {Vec2Maths::add(a, toAdd), Vec2Maths::add(b, toAdd)}; 20 | } 21 | 22 | /** 23 | * @return A copy of the line segment, offset by the given vector. 24 | */ 25 | LineSegment operator-(const Vec2 &toRemove) const { 26 | return {Vec2Maths::subtract(a, toRemove), Vec2Maths::subtract(b, toRemove)}; 27 | } 28 | 29 | /** 30 | * @return The line segment's normal vector. 31 | */ 32 | Vec2 normal() const { 33 | auto dir = direction(); 34 | 35 | // return the direction vector 36 | // rotated by 90 degrees counter-clockwise 37 | return {-dir.y, dir.x}; 38 | } 39 | 40 | /** 41 | * @return The line segment's direction vector. 42 | */ 43 | Vec2 direction(bool normalized = true) const { 44 | auto vec = Vec2Maths::subtract(b, a); 45 | 46 | return normalized 47 | ? Vec2Maths::normalized(vec) 48 | : vec; 49 | } 50 | 51 | static std::optional intersection(const LineSegment &a, const LineSegment &b, bool infiniteLines) { 52 | // calculate un-normalized direction vectors 53 | auto r = a.direction(false); 54 | auto s = b.direction(false); 55 | 56 | auto originDist = Vec2Maths::subtract(b.a, a.a); 57 | 58 | auto uNumerator = Vec2Maths::cross(originDist, r); 59 | auto denominator = Vec2Maths::cross(r, s); 60 | 61 | if (std::abs(denominator) < 0.0001f) { 62 | // The lines are parallel 63 | return std::nullopt; 64 | } 65 | 66 | // solve the intersection positions 67 | auto u = uNumerator / denominator; 68 | auto t = Vec2Maths::cross(originDist, s) / denominator; 69 | 70 | if (!infiniteLines && (t < 0 || t > 1 || u < 0 || u > 1)) { 71 | // the intersection lies outside of the line segments 72 | return std::nullopt; 73 | } 74 | 75 | // calculate the intersection point 76 | // a.a + r * t; 77 | return Vec2Maths::add(a.a, Vec2Maths::multiply(r, t)); 78 | } 79 | }; 80 | 81 | 82 | } // namespace crushedpixel -------------------------------------------------------------------------------- /include/Vec2.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace crushedpixel { 6 | 7 | /** 8 | * A two-dimensional float vector. 9 | * It exposes the x and y fields 10 | * as required by the Polyline2D functions. 11 | */ 12 | struct Vec2 { 13 | Vec2() : 14 | Vec2(0, 0) {} 15 | 16 | Vec2(float x, float y) : 17 | x(x), y(y) {} 18 | 19 | virtual ~Vec2() = default; 20 | 21 | float x, y; 22 | }; 23 | 24 | namespace Vec2Maths { 25 | 26 | template 27 | static bool equal(const Vec2 &a, const Vec2 &b) { 28 | return a.x == b.x && a.y == b.y; 29 | } 30 | 31 | template 32 | static Vec2 multiply(const Vec2 &a, const Vec2 &b) { 33 | return {a.x * b.x, a.y * b.y}; 34 | } 35 | 36 | template 37 | static Vec2 multiply(const Vec2 &vec, float factor) { 38 | return {vec.x * factor, vec.y * factor}; 39 | } 40 | 41 | template 42 | static Vec2 divide(const Vec2 &vec, float factor) { 43 | return {vec.x / factor, vec.y / factor}; 44 | } 45 | 46 | template 47 | static Vec2 add(const Vec2 &a, const Vec2 &b) { 48 | return {a.x + b.x, a.y + b.y}; 49 | } 50 | 51 | template 52 | static Vec2 subtract(const Vec2 &a, const Vec2 &b) { 53 | return {a.x - b.x, a.y - b.y}; 54 | } 55 | 56 | template 57 | static float magnitude(const Vec2 &vec) { 58 | return std::sqrt(vec.x * vec.x + vec.y * vec.y); 59 | } 60 | 61 | template 62 | static Vec2 withLength(const Vec2 &vec, float len) { 63 | auto mag = magnitude(vec); 64 | auto factor = mag / len; 65 | return divide(vec, factor); 66 | } 67 | 68 | template 69 | static Vec2 normalized(const Vec2 &vec) { 70 | return withLength(vec, 1); 71 | } 72 | 73 | /** 74 | * Calculates the dot product of two vectors. 75 | */ 76 | template 77 | static float dot(const Vec2 &a, const Vec2 &b) { 78 | return a.x * b.x + a.y * b.y; 79 | } 80 | 81 | /** 82 | * Calculates the cross product of two vectors. 83 | */ 84 | template 85 | static float cross(const Vec2 &a, const Vec2 &b) { 86 | return a.x * b.y - a.y * b.x; 87 | } 88 | 89 | /** 90 | * Calculates the angle between two vectors. 91 | */ 92 | template 93 | static float angle(const Vec2 &a, const Vec2 &b) { 94 | return std::acos(dot(a, b) / (magnitude(a) * magnitude(b))); 95 | } 96 | 97 | } // namespace Vec2Maths 98 | 99 | } -------------------------------------------------------------------------------- /include/Polyline2D.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "LineSegment.h" 4 | #include 5 | #include 6 | #include 7 | 8 | namespace crushedpixel { 9 | 10 | class Polyline2D { 11 | public: 12 | enum class JointStyle { 13 | /** 14 | * Corners are drawn with sharp joints. 15 | * If the joint's outer angle is too large, 16 | * the joint is drawn as beveled instead, 17 | * to avoid the miter extending too far out. 18 | */ 19 | MITER, 20 | /** 21 | * Corners are flattened. 22 | */ 23 | BEVEL, 24 | /** 25 | * Corners are rounded off. 26 | */ 27 | ROUND 28 | }; 29 | 30 | enum class EndCapStyle { 31 | /** 32 | * Path ends are drawn flat, 33 | * and don't exceed the actual end point. 34 | */ 35 | BUTT, // lol 36 | /** 37 | * Path ends are drawn flat, 38 | * but extended beyond the end point 39 | * by half the line thickness. 40 | */ 41 | SQUARE, 42 | /** 43 | * Path ends are rounded off. 44 | */ 45 | ROUND, 46 | /** 47 | * Path ends are connected according to the JointStyle. 48 | * When using this EndCapStyle, don't specify the common start/end point twice, 49 | * as Polyline2D connects the first and last input point itself. 50 | */ 51 | JOINT 52 | }; 53 | 54 | /** 55 | * Creates a vector of vertices describing a solid path through the input points. 56 | * @param points The points of the path. 57 | * @param thickness The path's thickness. 58 | * @param jointStyle The path's joint style. 59 | * @param endCapStyle The path's end cap style. 60 | * @param allowOverlap Whether to allow overlapping vertices. 61 | * This yields better results when dealing with paths 62 | * whose points have a distance smaller than the thickness, 63 | * but may introduce overlapping vertices, 64 | * which is undesirable when rendering transparent paths. 65 | * @return The vertices describing the path. 66 | * @tparam Vec2 The vector type to use for the vertices. 67 | * Must have public non-const float fields "x" and "y". 68 | * Must have a two-args constructor taking x and y values. 69 | * See crushedpixel::Vec2 for a type that satisfies these requirements. 70 | * @tparam InputCollection The collection type of the input points. 71 | * Must contain elements of type Vec2. 72 | * Must expose size() and operator[] functions. 73 | */ 74 | template 75 | static std::vector create(const InputCollection &points, float thickness, 76 | JointStyle jointStyle = JointStyle::MITER, 77 | EndCapStyle endCapStyle = EndCapStyle::BUTT, 78 | bool allowOverlap = false) { 79 | std::vector vertices; 80 | create(vertices, points, thickness, jointStyle, endCapStyle, allowOverlap); 81 | return vertices; 82 | } 83 | 84 | template 85 | static std::vector create(const std::vector &points, float thickness, 86 | JointStyle jointStyle = JointStyle::MITER, 87 | EndCapStyle endCapStyle = EndCapStyle::BUTT, 88 | bool allowOverlap = false) { 89 | std::vector vertices; 90 | create>(vertices, points, thickness, jointStyle, endCapStyle, allowOverlap); 91 | return vertices; 92 | } 93 | 94 | template 95 | static size_t create(std::vector &vertices, const InputCollection &points, float thickness, 96 | JointStyle jointStyle = JointStyle::MITER, 97 | EndCapStyle endCapStyle = EndCapStyle::BUTT, 98 | bool allowOverlap = false) { 99 | auto numVerticesBefore = vertices.size(); 100 | 101 | create(std::back_inserter(vertices), points, thickness, 102 | jointStyle, endCapStyle, allowOverlap); 103 | 104 | return vertices.size() - numVerticesBefore; 105 | } 106 | 107 | template 108 | static OutputIterator create(OutputIterator vertices, const InputCollection &points, float thickness, 109 | JointStyle jointStyle = JointStyle::MITER, 110 | EndCapStyle endCapStyle = EndCapStyle::BUTT, 111 | bool allowOverlap = false) { 112 | // operate on half the thickness to make our lives easier 113 | thickness /= 2; 114 | 115 | // create poly segments from the points 116 | std::vector> segments; 117 | for (size_t i = 0; i + 1 < points.size(); i++) { 118 | auto &point1 = points[i]; 119 | auto &point2 = points[i + 1]; 120 | 121 | // to avoid division-by-zero errors, 122 | // only create a line segment for non-identical points 123 | if (!Vec2Maths::equal(point1, point2)) { 124 | segments.emplace_back(LineSegment(point1, point2), thickness); 125 | } 126 | } 127 | 128 | if (endCapStyle == EndCapStyle::JOINT) { 129 | // create a connecting segment from the last to the first point 130 | 131 | auto &point1 = points[points.size() - 1]; 132 | auto &point2 = points[0]; 133 | 134 | // to avoid division-by-zero errors, 135 | // only create a line segment for non-identical points 136 | if (!Vec2Maths::equal(point1, point2)) { 137 | segments.emplace_back(LineSegment(point1, point2), thickness); 138 | } 139 | } 140 | 141 | if (segments.empty()) { 142 | // handle the case of insufficient input points 143 | return vertices; 144 | } 145 | 146 | Vec2 nextStart1{0, 0}; 147 | Vec2 nextStart2{0, 0}; 148 | Vec2 start1{0, 0}; 149 | Vec2 start2{0, 0}; 150 | Vec2 end1{0, 0}; 151 | Vec2 end2{0, 0}; 152 | 153 | // calculate the path's global start and end points 154 | auto &firstSegment = segments[0]; 155 | auto &lastSegment = segments[segments.size() - 1]; 156 | 157 | auto pathStart1 = firstSegment.edge1.a; 158 | auto pathStart2 = firstSegment.edge2.a; 159 | auto pathEnd1 = lastSegment.edge1.b; 160 | auto pathEnd2 = lastSegment.edge2.b; 161 | 162 | // handle different end cap styles 163 | if (endCapStyle == EndCapStyle::SQUARE) { 164 | // extend the start/end points by half the thickness 165 | pathStart1 = Vec2Maths::subtract(pathStart1, Vec2Maths::multiply(firstSegment.edge1.direction(), thickness)); 166 | pathStart2 = Vec2Maths::subtract(pathStart2, Vec2Maths::multiply(firstSegment.edge2.direction(), thickness)); 167 | pathEnd1 = Vec2Maths::add(pathEnd1, Vec2Maths::multiply(lastSegment.edge1.direction(), thickness)); 168 | pathEnd2 = Vec2Maths::add(pathEnd2, Vec2Maths::multiply(lastSegment.edge2.direction(), thickness)); 169 | 170 | } else if (endCapStyle == EndCapStyle::ROUND) { 171 | // draw half circle end caps 172 | createTriangleFan(vertices, firstSegment.center.a, firstSegment.center.a, 173 | firstSegment.edge1.a, firstSegment.edge2.a, false); 174 | createTriangleFan(vertices, lastSegment.center.b, lastSegment.center.b, 175 | lastSegment.edge1.b, lastSegment.edge2.b, true); 176 | 177 | } else if (endCapStyle == EndCapStyle::JOINT) { 178 | // join the last (connecting) segment and the first segment 179 | createJoint(vertices, lastSegment, firstSegment, jointStyle, 180 | pathEnd1, pathEnd2, pathStart1, pathStart2, allowOverlap); 181 | } 182 | 183 | // generate mesh data for path segments 184 | for (size_t i = 0; i < segments.size(); i++) { 185 | auto &segment = segments[i]; 186 | 187 | // calculate start 188 | if (i == 0) { 189 | // this is the first segment 190 | start1 = pathStart1; 191 | start2 = pathStart2; 192 | } 193 | 194 | if (i + 1 == segments.size()) { 195 | // this is the last segment 196 | end1 = pathEnd1; 197 | end2 = pathEnd2; 198 | 199 | } else { 200 | createJoint(vertices, segment, segments[i + 1], jointStyle, 201 | end1, end2, nextStart1, nextStart2, allowOverlap); 202 | } 203 | 204 | // emit vertices 205 | *vertices++ = start1; 206 | *vertices++ = start2; 207 | *vertices++ = end1; 208 | 209 | *vertices++ = end1; 210 | *vertices++ = start2; 211 | *vertices++ = end2; 212 | 213 | start1 = nextStart1; 214 | start2 = nextStart2; 215 | } 216 | 217 | return vertices; 218 | } 219 | 220 | private: 221 | static constexpr float pi = 3.14159265358979323846f; 222 | 223 | /** 224 | * The threshold for mitered joints. 225 | * If the joint's angle is smaller than this angle, 226 | * the joint will be drawn beveled instead. 227 | */ 228 | static constexpr float miterMinAngle = 0.349066; // ~20 degrees 229 | 230 | /** 231 | * The minimum angle of a round joint's triangles. 232 | */ 233 | static constexpr float roundMinAngle = 0.174533; // ~10 degrees 234 | 235 | template 236 | struct PolySegment { 237 | PolySegment(const LineSegment ¢er, float thickness) : 238 | center(center), 239 | // calculate the segment's outer edges by offsetting 240 | // the central line by the normal vector 241 | // multiplied with the thickness 242 | 243 | // center + center.normal() * thickness 244 | edge1(center + Vec2Maths::multiply(center.normal(), thickness)), 245 | edge2(center - Vec2Maths::multiply(center.normal(), thickness)) {} 246 | 247 | LineSegment center, edge1, edge2; 248 | }; 249 | 250 | template 251 | static OutputIterator createJoint(OutputIterator vertices, 252 | const PolySegment &segment1, const PolySegment &segment2, 253 | JointStyle jointStyle, Vec2 &end1, Vec2 &end2, 254 | Vec2 &nextStart1, Vec2 &nextStart2, 255 | bool allowOverlap) { 256 | // calculate the angle between the two line segments 257 | auto dir1 = segment1.center.direction(); 258 | auto dir2 = segment2.center.direction(); 259 | 260 | auto angle = Vec2Maths::angle(dir1, dir2); 261 | 262 | // wrap the angle around the 180° mark if it exceeds 90° 263 | // for minimum angle detection 264 | auto wrappedAngle = angle; 265 | if (wrappedAngle > pi / 2) { 266 | wrappedAngle = pi - wrappedAngle; 267 | } 268 | 269 | if (jointStyle == JointStyle::MITER && wrappedAngle < miterMinAngle) { 270 | // the minimum angle for mitered joints wasn't exceeded. 271 | // to avoid the intersection point being extremely far out, 272 | // thus producing an enormous joint like a rasta on 4/20, 273 | // we render the joint beveled instead. 274 | jointStyle = JointStyle::BEVEL; 275 | } 276 | 277 | if (jointStyle == JointStyle::MITER) { 278 | // calculate each edge's intersection point 279 | // with the next segment's central line 280 | auto sec1 = LineSegment::intersection(segment1.edge1, segment2.edge1, true); 281 | auto sec2 = LineSegment::intersection(segment1.edge2, segment2.edge2, true); 282 | 283 | end1 = sec1 ? *sec1 : segment1.edge1.b; 284 | end2 = sec2 ? *sec2 : segment1.edge2.b; 285 | 286 | nextStart1 = end1; 287 | nextStart2 = end2; 288 | 289 | } else { 290 | // joint style is either BEVEL or ROUND 291 | 292 | // find out which are the inner edges for this joint 293 | auto x1 = dir1.x; 294 | auto x2 = dir2.x; 295 | auto y1 = dir1.y; 296 | auto y2 = dir2.y; 297 | 298 | auto clockwise = x1 * y2 - x2 * y1 < 0; 299 | 300 | const LineSegment *inner1, *inner2, *outer1, *outer2; 301 | 302 | // as the normal vector is rotated counter-clockwise, 303 | // the first edge lies to the left 304 | // from the central line's perspective, 305 | // and the second one to the right. 306 | if (clockwise) { 307 | outer1 = &segment1.edge1; 308 | outer2 = &segment2.edge1; 309 | inner1 = &segment1.edge2; 310 | inner2 = &segment2.edge2; 311 | } else { 312 | outer1 = &segment1.edge2; 313 | outer2 = &segment2.edge2; 314 | inner1 = &segment1.edge1; 315 | inner2 = &segment2.edge1; 316 | } 317 | 318 | // calculate the intersection point of the inner edges 319 | auto innerSecOpt = LineSegment::intersection(*inner1, *inner2, allowOverlap); 320 | 321 | auto innerSec = innerSecOpt 322 | ? *innerSecOpt 323 | // for parallel lines, simply connect them directly 324 | : inner1->b; 325 | 326 | // if there's no inner intersection, flip 327 | // the next start position for near-180° turns 328 | Vec2 innerStart; 329 | if (innerSecOpt) { 330 | innerStart = innerSec; 331 | } else if (angle > pi / 2) { 332 | innerStart = outer1->b; 333 | } else { 334 | innerStart = inner1->b; 335 | } 336 | 337 | if (clockwise) { 338 | end1 = outer1->b; 339 | end2 = innerSec; 340 | 341 | nextStart1 = outer2->a; 342 | nextStart2 = innerStart; 343 | 344 | } else { 345 | end1 = innerSec; 346 | end2 = outer1->b; 347 | 348 | nextStart1 = innerStart; 349 | nextStart2 = outer2->a; 350 | } 351 | 352 | // connect the intersection points according to the joint style 353 | 354 | if (jointStyle == JointStyle::BEVEL) { 355 | // simply connect the intersection points 356 | *vertices++ = outer1->b; 357 | *vertices++ = outer2->a; 358 | *vertices++ = innerSec; 359 | 360 | } else if (jointStyle == JointStyle::ROUND) { 361 | // draw a circle between the ends of the outer edges, 362 | // centered at the actual point 363 | // with half the line thickness as the radius 364 | createTriangleFan(vertices, innerSec, segment1.center.b, outer1->b, outer2->a, clockwise); 365 | } else { 366 | assert(false); 367 | } 368 | } 369 | 370 | return vertices; 371 | } 372 | 373 | /** 374 | * Creates a partial circle between two points. 375 | * The points must be equally far away from the origin. 376 | * @param vertices The vector to add vertices to. 377 | * @param connectTo The position to connect the triangles to. 378 | * @param origin The circle's origin. 379 | * @param start The circle's starting point. 380 | * @param end The circle's ending point. 381 | * @param clockwise Whether the circle's rotation is clockwise. 382 | */ 383 | template 384 | static OutputIterator createTriangleFan(OutputIterator vertices, Vec2 connectTo, Vec2 origin, 385 | Vec2 start, Vec2 end, bool clockwise) { 386 | 387 | auto point1 = Vec2Maths::subtract(start, origin); 388 | auto point2 = Vec2Maths::subtract(end, origin); 389 | 390 | // calculate the angle between the two points 391 | auto angle1 = atan2(point1.y, point1.x); 392 | auto angle2 = atan2(point2.y, point2.x); 393 | 394 | // ensure the outer angle is calculated 395 | if (clockwise) { 396 | if (angle2 > angle1) { 397 | angle2 = angle2 - 2 * pi; 398 | } 399 | } else { 400 | if (angle1 > angle2) { 401 | angle1 = angle1 - 2 * pi; 402 | } 403 | } 404 | 405 | auto jointAngle = angle2 - angle1; 406 | 407 | // calculate the amount of triangles to use for the joint 408 | auto numTriangles = std::max(1, (int) std::floor(std::abs(jointAngle) / roundMinAngle)); 409 | 410 | // calculate the angle of each triangle 411 | auto triAngle = jointAngle / numTriangles; 412 | 413 | Vec2 startPoint = start; 414 | Vec2 endPoint; 415 | for (int t = 0; t < numTriangles; t++) { 416 | if (t + 1 == numTriangles) { 417 | // it's the last triangle - ensure it perfectly 418 | // connects to the next line 419 | endPoint = end; 420 | } else { 421 | auto rot = (t + 1) * triAngle; 422 | 423 | // rotate the original point around the origin 424 | endPoint.x = std::cos(rot) * point1.x - std::sin(rot) * point1.y; 425 | endPoint.y = std::sin(rot) * point1.x + std::cos(rot) * point1.y; 426 | 427 | // re-add the rotation origin to the target point 428 | endPoint = Vec2Maths::add(endPoint, origin); 429 | } 430 | 431 | // emit the triangle 432 | *vertices++ = startPoint; 433 | *vertices++ = endPoint; 434 | *vertices++ = connectTo; 435 | 436 | startPoint = endPoint; 437 | } 438 | 439 | return vertices; 440 | } 441 | }; 442 | 443 | } // namespace crushedpixel 444 | --------------------------------------------------------------------------------