| 1 | // Aseprite |
| 2 | // Copyright (C) 2018-2022 Igara Studio S.A. |
| 3 | // Copyright (C) 2001-2018 David Capello |
| 4 | // |
| 5 | // This program is distributed under the terms of |
| 6 | // the End-User License Agreement for Aseprite. |
| 7 | |
| 8 | #include "base/pi.h" |
| 9 | #include "doc/layer_tilemap.h" |
| 10 | |
| 11 | namespace app { |
| 12 | namespace tools { |
| 13 | |
| 14 | struct LineData2 { |
| 15 | Intertwine::LineData head; |
| 16 | Stroke& output; |
| 17 | LineData2(ToolLoop* loop, const Stroke::Pt& a, const Stroke::Pt& b, |
| 18 | Stroke& output) |
| 19 | : head(loop, a, b) |
| 20 | , output(output) { |
| 21 | } |
| 22 | }; |
| 23 | |
| 24 | static void addPointsWithoutDuplicatingLastOne(int x, int y, LineData2* data) |
| 25 | { |
| 26 | data->head.doStep(x, y); |
| 27 | if (data->output.empty() || |
| 28 | data->output.lastPoint() != data->head.pt) { |
| 29 | data->output.addPoint(data->head.pt); |
| 30 | } |
| 31 | } |
| 32 | |
| 33 | class IntertwineNone : public Intertwine { |
| 34 | public: |
| 35 | |
| 36 | void joinStroke(ToolLoop* loop, const Stroke& stroke) override { |
| 37 | for (int c=0; c<stroke.size(); ++c) |
| 38 | doPointshapeStrokePt(stroke[c], loop); |
| 39 | } |
| 40 | |
| 41 | void fillStroke(ToolLoop* loop, const Stroke& stroke) override { |
| 42 | joinStroke(loop, stroke); |
| 43 | } |
| 44 | }; |
| 45 | |
| 46 | class IntertwineFirstPoint : public Intertwine { |
| 47 | public: |
| 48 | // Snap angle because the angle between the first point and the last |
| 49 | // point might be useful for the ink (e.g. the gradient ink) |
| 50 | bool snapByAngle() override { return true; } |
| 51 | |
| 52 | void joinStroke(ToolLoop* loop, const Stroke& stroke) override { |
| 53 | if (stroke.empty()) |
| 54 | return; |
| 55 | |
| 56 | gfx::Point mid; |
| 57 | |
| 58 | if (loop->getController()->isTwoPoints() && |
| 59 | (int(loop->getModifiers()) & int(ToolLoopModifiers::kFromCenter))) { |
| 60 | int n = 0; |
| 61 | for (auto& pt : stroke) { |
| 62 | mid.x += pt.x; |
| 63 | mid.y += pt.y; |
| 64 | ++n; |
| 65 | } |
| 66 | mid.x /= n; |
| 67 | mid.y /= n; |
| 68 | } |
| 69 | else { |
| 70 | mid = stroke[0].toPoint(); |
| 71 | } |
| 72 | |
| 73 | doPointshapePoint(mid.x, mid.y, loop); |
| 74 | } |
| 75 | |
| 76 | void fillStroke(ToolLoop* loop, const Stroke& stroke) override { |
| 77 | joinStroke(loop, stroke); |
| 78 | } |
| 79 | }; |
| 80 | |
| 81 | class IntertwineAsLines : public Intertwine { |
| 82 | // It was introduced to know if joinStroke function |
| 83 | // was executed inmediatelly after a "Last" trace policy (i.e. after the |
| 84 | // user confirms a line draw while he is holding down the SHIFT key), so |
| 85 | // we have to ignore printing the first pixel of the line. |
| 86 | bool m_retainedTracePolicyLast = false; |
| 87 | |
| 88 | // In freehand-like tools, on each mouse movement we draw only the |
| 89 | // line between the last two mouse points in the stroke (the |
| 90 | // complete stroke is not re-painted again), so we want to indicate |
| 91 | // if this is the first stroke of all (the only one that needs the |
| 92 | // first pixel of the line algorithm) |
| 93 | bool m_firstStroke = true; |
| 94 | |
| 95 | public: |
| 96 | bool snapByAngle() override { return true; } |
| 97 | |
| 98 | void prepareIntertwine(ToolLoop* loop) override { |
| 99 | m_retainedTracePolicyLast = false; |
| 100 | m_firstStroke = true; |
| 101 | } |
| 102 | |
| 103 | void joinStroke(ToolLoop* loop, const Stroke& stroke) override { |
| 104 | // Required for LineFreehand controller in the first stage, when |
| 105 | // we are drawing the line and the trace policy is "Last". Each |
| 106 | // new joinStroke() is like a fresh start. Without this fix, the |
| 107 | // first stage on LineFreehand will draw a "star" like pattern |
| 108 | // with lines from the first point to the last point. |
| 109 | if (loop->getTracePolicy() == TracePolicy::Last) |
| 110 | m_retainedTracePolicyLast = true; |
| 111 | |
| 112 | if (stroke.size() == 0) |
| 113 | return; |
| 114 | else if (stroke.size() == 1) { |
| 115 | doPointshapeStrokePt(stroke[0], loop); |
| 116 | } |
| 117 | else { |
| 118 | Stroke pts; |
| 119 | for (int c=0; c+1<stroke.size(); ++c) { |
| 120 | auto lineAlgo = getLineAlgo(loop, stroke[c], stroke[c+1]); |
| 121 | LineData2 lineData(loop, stroke[c], stroke[c+1], pts); |
| 122 | lineAlgo(stroke[c].x, stroke[c].y, |
| 123 | stroke[c+1].x, stroke[c+1].y, |
| 124 | (void*)&lineData, |
| 125 | (AlgoPixel)&addPointsWithoutDuplicatingLastOne); |
| 126 | } |
| 127 | |
| 128 | // Don't draw the first point in freehand tools (this is to |
| 129 | // avoid painting above the last pixel of a freehand stroke, |
| 130 | // when we use Shift+click in the Pencil tool to continue the |
| 131 | // old stroke). |
| 132 | // TODO useful only in the case when brush size = 1px |
| 133 | const int start = |
| 134 | (loop->getController()->isFreehand() && |
| 135 | (m_retainedTracePolicyLast || !m_firstStroke) ? 1: 0); |
| 136 | |
| 137 | for (int c=start; c<pts.size(); ++c) |
| 138 | doPointshapeStrokePt(pts[c], loop); |
| 139 | |
| 140 | // Closed shape (polygon outline) |
| 141 | // Note: Contour tool was getting into the condition with no need, so |
| 142 | // we add the && !isFreehand to detect this circunstance. |
| 143 | // When this is missing, we have problems previewing the stroke of |
| 144 | // contour tool, with brush type = kImageBrush with alpha content and |
| 145 | // with not Pixel Perfect pencil mode. |
| 146 | if (loop->getFilled() && !loop->getController()->isFreehand()) { |
| 147 | doPointshapeLine(stroke[stroke.size()-1], stroke[0], loop); |
| 148 | } |
| 149 | } |
| 150 | m_firstStroke = false; |
| 151 | } |
| 152 | |
| 153 | void fillStroke(ToolLoop* loop, const Stroke& stroke) override { |
| 154 | #if 0 |
| 155 | // We prefer to use doc::algorithm::polygon() directly instead of |
| 156 | // joinStroke() for the simplest cases (i.e. stroke.size() < 3 is |
| 157 | // one point, or a line with two points), because if we use |
| 158 | // joinStroke(), we'll get some undesirable behaviors of the |
| 159 | // Shift+click considerations. E.g. not drawing the first pixel |
| 160 | // (or nothing at all) because it can been seen as the |
| 161 | // continuation of the previous last point. An specific example is |
| 162 | // when stroke[0] == stroke[1], joinStroke() assumes that it has |
| 163 | // to draw a stroke with 2 pixels, but when the stroke is |
| 164 | // converted to "pts", "pts" has just one point, then if the first |
| 165 | // one has to be discarded no pixel is drawn. |
| 166 | if (stroke.size() < 3) { |
| 167 | joinStroke(loop, stroke); |
| 168 | return; |
| 169 | } |
| 170 | #endif |
| 171 | |
| 172 | // Fill content |
| 173 | auto v = stroke.toXYInts(); |
| 174 | doc::algorithm::polygon( |
| 175 | v.size()/2, &v[0], |
| 176 | loop, (AlgoHLine)doPointshapeHline); |
| 177 | } |
| 178 | |
| 179 | }; |
| 180 | |
| 181 | class IntertwineAsRectangles : public Intertwine { |
| 182 | public: |
| 183 | |
| 184 | void joinStroke(ToolLoop* loop, const Stroke& stroke) override { |
| 185 | if (stroke.size() == 0) |
| 186 | return; |
| 187 | |
| 188 | if (stroke.size() == 1) { |
| 189 | doPointshapePoint(stroke[0].x, stroke[0].y, loop); |
| 190 | } |
| 191 | else if (stroke.size() >= 2) { |
| 192 | for (int c=0; c+1<stroke.size(); ++c) { |
| 193 | // TODO fix this with strokes and dynamics |
| 194 | int x1 = stroke[c].x; |
| 195 | int y1 = stroke[c].y; |
| 196 | int x2 = stroke[c+1].x; |
| 197 | int y2 = stroke[c+1].y; |
| 198 | int y; |
| 199 | |
| 200 | if (x1 > x2) std::swap(x1, x2); |
| 201 | if (y1 > y2) std::swap(y1, y2); |
| 202 | |
| 203 | const double angle = loop->getController()->getShapeAngle(); |
| 204 | if (ABS(angle) < 0.001) { |
| 205 | doPointshapeLineWithoutDynamics(x1, y1, x2, y1, loop); |
| 206 | doPointshapeLineWithoutDynamics(x1, y2, x2, y2, loop); |
| 207 | |
| 208 | for (y=y1; y<=y2; y++) { |
| 209 | doPointshapePoint(x1, y, loop); |
| 210 | doPointshapePoint(x2, y, loop); |
| 211 | } |
| 212 | } |
| 213 | else { |
| 214 | Stroke p = rotateRectangle(x1, y1, x2, y2, angle); |
| 215 | int n = p.size(); |
| 216 | for (int i=0; i+1<n; ++i) { |
| 217 | doPointshapeLine(p[i], p[i+1], loop); |
| 218 | } |
| 219 | doPointshapeLine(p[n-1], p[0], loop); |
| 220 | } |
| 221 | } |
| 222 | } |
| 223 | } |
| 224 | |
| 225 | void fillStroke(ToolLoop* loop, const Stroke& stroke) override { |
| 226 | if (stroke.size() < 2) { |
| 227 | joinStroke(loop, stroke); |
| 228 | return; |
| 229 | } |
| 230 | |
| 231 | for (int c=0; c+1<stroke.size(); ++c) { |
| 232 | int x1 = stroke[c].x; |
| 233 | int y1 = stroke[c].y; |
| 234 | int x2 = stroke[c+1].x; |
| 235 | int y2 = stroke[c+1].y; |
| 236 | int y; |
| 237 | |
| 238 | if (x1 > x2) std::swap(x1, x2); |
| 239 | if (y1 > y2) std::swap(y1, y2); |
| 240 | |
| 241 | const double angle = loop->getController()->getShapeAngle(); |
| 242 | if (ABS(angle) < 0.001) { |
| 243 | for (y=y1; y<=y2; y++) |
| 244 | doPointshapeLineWithoutDynamics(x1, y, x2, y, loop); |
| 245 | } |
| 246 | else { |
| 247 | Stroke p = rotateRectangle(x1, y1, x2, y2, angle); |
| 248 | auto v = p.toXYInts(); |
| 249 | doc::algorithm::polygon( |
| 250 | v.size()/2, &v[0], |
| 251 | loop, (AlgoHLine)doPointshapeHline); |
| 252 | } |
| 253 | } |
| 254 | } |
| 255 | |
| 256 | gfx::Rect getStrokeBounds(ToolLoop* loop, const Stroke& stroke) override { |
| 257 | gfx::Rect bounds = stroke.bounds(); |
| 258 | const double angle = loop->getController()->getShapeAngle(); |
| 259 | |
| 260 | if (ABS(angle) > 0.001) { |
| 261 | bounds = gfx::Rect(); |
| 262 | if (stroke.size() >= 2) { |
| 263 | for (int c=0; c+1<stroke.size(); ++c) { |
| 264 | int x1 = stroke[c].x; |
| 265 | int y1 = stroke[c].y; |
| 266 | int x2 = stroke[c+1].x; |
| 267 | int y2 = stroke[c+1].y; |
| 268 | bounds |= rotateRectangle(x1, y1, x2, y2, angle).bounds(); |
| 269 | } |
| 270 | } |
| 271 | } |
| 272 | |
| 273 | return bounds; |
| 274 | } |
| 275 | |
| 276 | private: |
| 277 | static Stroke rotateRectangle(int x1, int y1, int x2, int y2, double angle) { |
| 278 | int cx = (x1+x2)/2; |
| 279 | int cy = (y1+y2)/2; |
| 280 | int a = (x2-x1)/2; |
| 281 | int b = (y2-y1)/2; |
| 282 | double s = -std::sin(angle); |
| 283 | double c = std::cos(angle); |
| 284 | |
| 285 | Stroke stroke; |
| 286 | stroke.addPoint(Point(cx-a*c-b*s, cy+a*s-b*c)); |
| 287 | stroke.addPoint(Point(cx+a*c-b*s, cy-a*s-b*c)); |
| 288 | stroke.addPoint(Point(cx+a*c+b*s, cy-a*s+b*c)); |
| 289 | stroke.addPoint(Point(cx-a*c+b*s, cy+a*s+b*c)); |
| 290 | return stroke; |
| 291 | } |
| 292 | |
| 293 | }; |
| 294 | |
| 295 | class IntertwineAsEllipses : public Intertwine { |
| 296 | public: |
| 297 | |
| 298 | void joinStroke(ToolLoop* loop, const Stroke& stroke) override { |
| 299 | if (stroke.size() == 0) |
| 300 | return; |
| 301 | |
| 302 | if (stroke.size() == 1) { |
| 303 | doPointshapePoint(stroke[0].x, stroke[0].y, loop); |
| 304 | } |
| 305 | else if (stroke.size() >= 2) { |
| 306 | for (int c=0; c+1<stroke.size(); ++c) { |
| 307 | int x1 = stroke[c].x; |
| 308 | int y1 = stroke[c].y; |
| 309 | int x2 = stroke[c+1].x; |
| 310 | int y2 = stroke[c+1].y; |
| 311 | |
| 312 | if (x1 > x2) std::swap(x1, x2); |
| 313 | if (y1 > y2) std::swap(y1, y2); |
| 314 | |
| 315 | const double angle = loop->getController()->getShapeAngle(); |
| 316 | if (ABS(angle) < 0.001) { |
| 317 | algo_ellipse(x1, y1, x2, y2, 0, 0, loop, (AlgoPixel)doPointshapePoint); |
| 318 | } |
| 319 | else { |
| 320 | draw_rotated_ellipse((x1+x2)/2, (y1+y2)/2, |
| 321 | ABS(x2-x1)/2, |
| 322 | ABS(y2-y1)/2, |
| 323 | angle, |
| 324 | loop, (AlgoPixel)doPointshapePoint); |
| 325 | } |
| 326 | } |
| 327 | } |
| 328 | } |
| 329 | |
| 330 | void fillStroke(ToolLoop* loop, const Stroke& stroke) override { |
| 331 | if (stroke.size() < 2) { |
| 332 | joinStroke(loop, stroke); |
| 333 | return; |
| 334 | } |
| 335 | |
| 336 | for (int c=0; c+1<stroke.size(); ++c) { |
| 337 | int x1 = stroke[c].x; |
| 338 | int y1 = stroke[c].y; |
| 339 | int x2 = stroke[c+1].x; |
| 340 | int y2 = stroke[c+1].y; |
| 341 | |
| 342 | if (x1 > x2) std::swap(x1, x2); |
| 343 | if (y1 > y2) std::swap(y1, y2); |
| 344 | |
| 345 | const double angle = loop->getController()->getShapeAngle(); |
| 346 | if (ABS(angle) < 0.001) { |
| 347 | algo_ellipsefill(x1, y1, x2, y2, 0, 0, loop, (AlgoHLine)doPointshapeHline); |
| 348 | } |
| 349 | else { |
| 350 | fill_rotated_ellipse((x1+x2)/2, (y1+y2)/2, |
| 351 | ABS(x2-x1)/2, |
| 352 | ABS(y2-y1)/2, |
| 353 | angle, |
| 354 | loop, (AlgoHLine)doPointshapeHline); |
| 355 | } |
| 356 | } |
| 357 | } |
| 358 | |
| 359 | gfx::Rect getStrokeBounds(ToolLoop* loop, const Stroke& stroke) override { |
| 360 | gfx::Rect bounds = stroke.bounds(); |
| 361 | const double angle = loop->getController()->getShapeAngle(); |
| 362 | |
| 363 | if (ABS(angle) > 0.001) { |
| 364 | Point center = bounds.center(); |
| 365 | int a = bounds.w/2.0 + 0.5; |
| 366 | int b = bounds.h/2.0 + 0.5; |
| 367 | double xd = a*a; |
| 368 | double yd = b*b; |
| 369 | double s = std::sin(angle); |
| 370 | double zd = (xd-yd)*s; |
| 371 | |
| 372 | a = std::sqrt(xd-zd*s) + 0.5; |
| 373 | b = std::sqrt(yd+zd*s) + 0.5; |
| 374 | |
| 375 | bounds.x = center.x-a-1; |
| 376 | bounds.y = center.y-b-1; |
| 377 | bounds.w = 2*a+3; |
| 378 | bounds.h = 2*b+3; |
| 379 | } |
| 380 | else { |
| 381 | ++bounds.w; |
| 382 | ++bounds.h; |
| 383 | } |
| 384 | |
| 385 | return bounds; |
| 386 | } |
| 387 | |
| 388 | }; |
| 389 | |
| 390 | class IntertwineAsBezier : public Intertwine { |
| 391 | public: |
| 392 | |
| 393 | void joinStroke(ToolLoop* loop, const Stroke& stroke) override { |
| 394 | if (stroke.size() == 0) |
| 395 | return; |
| 396 | |
| 397 | for (int c=0; c<stroke.size(); c += 4) { |
| 398 | if (stroke.size()-c == 1) { |
| 399 | doPointshapeStrokePt(stroke[c], loop); |
| 400 | } |
| 401 | else if (stroke.size()-c == 2) { |
| 402 | doPointshapeLine(stroke[c], stroke[c+1], loop); |
| 403 | } |
| 404 | else if (stroke.size()-c == 3) { |
| 405 | algo_spline(stroke[c ].x, stroke[c ].y, |
| 406 | stroke[c+1].x, stroke[c+1].y, |
| 407 | stroke[c+1].x, stroke[c+1].y, |
| 408 | stroke[c+2].x, stroke[c+2].y, loop, |
| 409 | (AlgoLine)doPointshapeLineWithoutDynamics); |
| 410 | } |
| 411 | else { |
| 412 | algo_spline(stroke[c ].x, stroke[c ].y, |
| 413 | stroke[c+1].x, stroke[c+1].y, |
| 414 | stroke[c+2].x, stroke[c+2].y, |
| 415 | stroke[c+3].x, stroke[c+3].y, loop, |
| 416 | (AlgoLine)doPointshapeLineWithoutDynamics); |
| 417 | } |
| 418 | } |
| 419 | } |
| 420 | |
| 421 | void fillStroke(ToolLoop* loop, const Stroke& stroke) override { |
| 422 | joinStroke(loop, stroke); |
| 423 | } |
| 424 | }; |
| 425 | |
| 426 | class IntertwineAsPixelPerfect : public Intertwine { |
| 427 | // It was introduced to know if joinStroke function |
| 428 | // was executed inmediatelly after a "Last" trace policy (i.e. after the |
| 429 | // user confirms a line draw while he is holding down the SHIFT key), so |
| 430 | // we have to ignore printing the first pixel of the line. |
| 431 | bool m_retainedTracePolicyLast = false; |
| 432 | Stroke m_pts; |
| 433 | bool m_saveStrokeArea = false; |
| 434 | |
| 435 | // Helper struct to store an image's area that will be affected by the stroke |
| 436 | // point at the specified position of the original image. |
| 437 | struct SavedArea { |
| 438 | doc::ImageRef img; |
| 439 | // Original stroke point position. |
| 440 | tools::Stroke::Pt pos; |
| 441 | // Area of the original image that was saved into img. |
| 442 | gfx::Rect r; |
| 443 | }; |
| 444 | // Holds the areas saved by savePointshapeStrokePtArea method and restored by |
| 445 | // restoreLastPts method. |
| 446 | std::vector<SavedArea> m_savedAreas; |
| 447 | // When a SavedArea is restored we add its Rect to this Region, then we use |
| 448 | // this to expand the modified region when editing a tilemap manually. |
| 449 | gfx::Region m_restoredRegion; |
| 450 | // Last point index. |
| 451 | int m_lastPti; |
| 452 | |
| 453 | // Temporal tileset with latest changes to be used by pixel perfect only when |
| 454 | // modifying a tilemap in Manual mode. |
| 455 | std::unique_ptr<Tileset> m_tempTileset; |
| 456 | doc::Grid m_grid; |
| 457 | doc::Grid m_dstGrid; |
| 458 | doc::Grid m_celGrid; |
| 459 | |
| 460 | public: |
| 461 | // Useful for Shift+Ctrl+pencil to draw straight lines and snap |
| 462 | // angle when "pixel perfect" is selected. |
| 463 | bool snapByAngle() override { return true; } |
| 464 | |
| 465 | void prepareIntertwine(ToolLoop* loop) override { |
| 466 | m_pts.reset(); |
| 467 | m_retainedTracePolicyLast = false; |
| 468 | m_grid = m_dstGrid = m_celGrid = loop->getGrid(); |
| 469 | m_restoredRegion.clear(); |
| 470 | |
| 471 | if (loop->getLayer()->isTilemap() && |
| 472 | !loop->isTilemapMode() && |
| 473 | loop->isManualTilesetMode()) { |
| 474 | const Tileset* srcTileset = static_cast<LayerTilemap*>(loop->getLayer())->tileset(); |
| 475 | m_tempTileset.reset(Tileset::MakeCopyCopyingImages(srcTileset)); |
| 476 | |
| 477 | // Grid to convert to dstImage coordinates |
| 478 | m_dstGrid.origin(gfx::Point(0, 0)); |
| 479 | |
| 480 | // Grid where the original cel is the origin |
| 481 | m_celGrid.origin(loop->getCel()->position()); |
| 482 | } |
| 483 | else { |
| 484 | m_tempTileset.reset(); |
| 485 | } |
| 486 | } |
| 487 | |
| 488 | void joinStroke(ToolLoop* loop, const Stroke& stroke) override { |
| 489 | // Required for LineFreehand controller in the first stage, when |
| 490 | // we are drawing the line and the trace policy is "Last". Each |
| 491 | // new joinStroke() is like a fresh start. Without this fix, the |
| 492 | // first stage on LineFreehand will draw a "star" like pattern |
| 493 | // with lines from the first point to the last point. |
| 494 | if (loop->getTracePolicy() == TracePolicy::Last) { |
| 495 | m_retainedTracePolicyLast = true; |
| 496 | m_pts.reset(); |
| 497 | } |
| 498 | |
| 499 | int thirdFromLastPt = 0, nextPt = 0; |
| 500 | |
| 501 | if (stroke.size() == 0) |
| 502 | return; |
| 503 | else if (stroke.size() == 1) { |
| 504 | if (m_pts.empty()) |
| 505 | m_pts = stroke; |
| 506 | |
| 507 | m_saveStrokeArea = false; |
| 508 | doPointshapeStrokePt(stroke[0], loop); |
| 509 | |
| 510 | return; |
| 511 | } |
| 512 | else { |
| 513 | if (stroke.firstPoint() == stroke.lastPoint()) |
| 514 | return; |
| 515 | |
| 516 | nextPt = m_pts.size(); |
| 517 | thirdFromLastPt = (m_pts.size() > 2 ? m_pts.size() - 3 : m_pts.size() - 1); |
| 518 | |
| 519 | for (int c=0; c+1<stroke.size(); ++c) { |
| 520 | auto lineAlgo = getLineAlgo(loop, stroke[c], stroke[c+1]); |
| 521 | LineData2 lineData(loop, stroke[c], stroke[c+1], m_pts); |
| 522 | lineAlgo( |
| 523 | stroke[c].x, |
| 524 | stroke[c].y, |
| 525 | stroke[c+1].x, |
| 526 | stroke[c+1].y, |
| 527 | (void*)&lineData, |
| 528 | (AlgoPixel)&addPointsWithoutDuplicatingLastOne); |
| 529 | } |
| 530 | } |
| 531 | |
| 532 | // For line brush type, the pixel-perfect will create gaps so we |
| 533 | // avoid removing points |
| 534 | if (loop->getBrush()->type() != kLineBrushType || |
| 535 | (loop->getDynamics().angle == tools::DynamicSensor::Static && |
| 536 | (loop->getBrush()->angle() == 0.0f || |
| 537 | loop->getBrush()->angle() == 90.0f || |
| 538 | loop->getBrush()->angle() == 180.0f))) { |
| 539 | for (int c=thirdFromLastPt; c<m_pts.size(); ++c) { |
| 540 | // We ignore a pixel that is between other two pixels in the |
| 541 | // corner of a L-like shape. |
| 542 | if (c > 0 && c+1 < m_pts.size() |
| 543 | && (m_pts[c-1].x == m_pts[c].x || m_pts[c-1].y == m_pts[c].y) |
| 544 | && (m_pts[c+1].x == m_pts[c].x || m_pts[c+1].y == m_pts[c].y) |
| 545 | && m_pts[c-1].x != m_pts[c+1].x |
| 546 | && m_pts[c-1].y != m_pts[c+1].y) { |
| 547 | restoreLastPts(loop, c, m_pts[c]); |
| 548 | if (c == nextPt-1) |
| 549 | nextPt--; |
| 550 | m_pts.erase(c); |
| 551 | } |
| 552 | } |
| 553 | } |
| 554 | |
| 555 | for (int c=nextPt; c<m_pts.size(); ++c) { |
| 556 | // We must ignore to print the first point of the line after |
| 557 | // a joinStroke pass with a retained "Last" trace policy |
| 558 | // (i.e. the user confirms draw a line while he is holding |
| 559 | // the SHIFT key)) |
| 560 | if (c == 0 && m_retainedTracePolicyLast) |
| 561 | continue; |
| 562 | |
| 563 | // For the last point we need to store the source image content at that |
| 564 | // point so we can restore it when erasing a point because of |
| 565 | // pixel-perfect. So we set the following flag to indicate this, and |
| 566 | // use it in doTransformPoint. |
| 567 | m_saveStrokeArea = (c == m_pts.size() - 1 && !m_retainedTracePolicyLast); |
| 568 | if (m_saveStrokeArea) { |
| 569 | clearPointshapeStrokePtAreas(); |
| 570 | setLastPtIndex(c); |
| 571 | } |
| 572 | doPointshapeStrokePt(m_pts[c], loop); |
| 573 | } |
| 574 | } |
| 575 | |
| 576 | void fillStroke(ToolLoop* loop, const Stroke& stroke) override { |
| 577 | if (stroke.empty()) |
| 578 | return; |
| 579 | |
| 580 | // Fill content |
| 581 | auto v = m_pts.toXYInts(); |
| 582 | doc::algorithm::polygon( |
| 583 | v.size()/2, &v[0], |
| 584 | loop, (AlgoHLine)doPointshapeHline); |
| 585 | } |
| 586 | |
| 587 | gfx::Region forceTilemapRegionToValidate() override { |
| 588 | return m_restoredRegion; |
| 589 | } |
| 590 | |
| 591 | protected: |
| 592 | void doTransformPoint(const Stroke::Pt& pt, ToolLoop* loop) override { |
| 593 | if (m_saveStrokeArea) |
| 594 | savePointshapeStrokePtArea(loop, pt); |
| 595 | |
| 596 | Intertwine::doTransformPoint(pt, loop); |
| 597 | |
| 598 | if (loop->getLayer()->isTilemap() && m_tempTileset) |
| 599 | updateTempTileset(loop, pt); |
| 600 | } |
| 601 | |
| 602 | private: |
| 603 | void clearPointshapeStrokePtAreas() { |
| 604 | m_savedAreas.clear(); |
| 605 | } |
| 606 | |
| 607 | void setLastPtIndex(const int pti) { |
| 608 | m_lastPti = pti; |
| 609 | } |
| 610 | |
| 611 | // Saves the destination image's area that will be updated by the point |
| 612 | // passed. The idea is to have the state of the image (only the |
| 613 | // portion modified by the stroke's point shape) before drawing the last |
| 614 | // point of the stroke, then if that point has to be deleted by the |
| 615 | // pixel-perfect algorithm, we can use this image to restore the image to the |
| 616 | // state previous to the deletion. This method is used by |
| 617 | // IntertwineAsPixelPerfect.joinStroke() method. |
| 618 | void savePointshapeStrokePtArea(ToolLoop* loop, const tools::Stroke::Pt& pt) { |
| 619 | gfx::Rect r; |
| 620 | loop->getPointShape()->getModifiedArea(loop, pt.x, pt.y, r); |
| 621 | |
| 622 | gfx::Region rgn(r); |
| 623 | // By wrapping the modified area's position when tiled mode is active, the |
| 624 | // user can draw outside the canvas and still get the pixel-perfect |
| 625 | // effect. |
| 626 | loop->getTiledModeHelper().wrapPosition(rgn); |
| 627 | loop->getTiledModeHelper().collapseRegionByTiledMode(rgn); |
| 628 | |
| 629 | for (auto a : rgn) { |
| 630 | a.offset(-loop->getCelOrigin()); |
| 631 | |
| 632 | if (m_tempTileset) { |
| 633 | forEachTilePos( |
| 634 | loop, m_dstGrid.tilesInCanvasRegion(gfx::Region(a)), |
| 635 | [loop](const doc::ImageRef existentTileImage, |
| 636 | const gfx::Point tilePos) { |
| 637 | loop->getDstImage()->copy( |
| 638 | existentTileImage.get(), |
| 639 | gfx::Clip(tilePos.x, tilePos.y, 0, 0, |
| 640 | existentTileImage->width(), |
| 641 | existentTileImage->height())); |
| 642 | }); |
| 643 | } |
| 644 | |
| 645 | ImageRef i(crop_image(loop->getDstImage(), a, loop->getDstImage()->maskColor())); |
| 646 | m_savedAreas.push_back(SavedArea{ i, pt, a }); |
| 647 | } |
| 648 | } |
| 649 | |
| 650 | // Takes the images saved by savePointshapeStrokePtArea and copies them to |
| 651 | // the destination image. It restores the destination image because the |
| 652 | // images in m_savedAreas are from previous states of the destination |
| 653 | // image. This method is used by IntertwineAsPixelPerfect.joinStroke() |
| 654 | // method. |
| 655 | void restoreLastPts(ToolLoop* loop, const int pti, const tools::Stroke::Pt& pt) { |
| 656 | if (m_savedAreas.empty() || pti != m_lastPti || m_savedAreas[0].pos != pt) |
| 657 | return; |
| 658 | |
| 659 | m_restoredRegion.clear(); |
| 660 | |
| 661 | tools::Stroke::Pt pos; |
| 662 | for (int i=0; i<m_savedAreas.size(); ++i) { |
| 663 | loop->getDstImage()->copy( |
| 664 | m_savedAreas[i].img.get(), |
| 665 | gfx::Clip(m_savedAreas[i].r.origin(), |
| 666 | m_savedAreas[i].img->bounds())); |
| 667 | |
| 668 | if (m_tempTileset) { |
| 669 | auto r = m_savedAreas[i].r; |
| 670 | forEachTilePos( |
| 671 | loop, m_dstGrid.tilesInCanvasRegion(gfx::Region(r)), |
| 672 | [this, i, r](const doc::ImageRef existentTileImage, |
| 673 | const gfx::Point tilePos) { |
| 674 | existentTileImage->copy( |
| 675 | m_savedAreas[i].img.get(), |
| 676 | gfx::Clip(r.x - tilePos.x, |
| 677 | r.y - tilePos.y, |
| 678 | 0, 0, r.w, r.h)); |
| 679 | }); |
| 680 | } |
| 681 | |
| 682 | m_restoredRegion |= gfx::Region(m_savedAreas[i].r); |
| 683 | } |
| 684 | |
| 685 | m_restoredRegion.offset(loop->getCelOrigin()); |
| 686 | } |
| 687 | |
| 688 | void updateTempTileset(ToolLoop* loop, const tools::Stroke::Pt& pt) { |
| 689 | ASSERT(m_tempTileset); |
| 690 | |
| 691 | gfx::Rect r; |
| 692 | loop->getPointShape()->getModifiedArea(loop, pt.x, pt.y, r); |
| 693 | |
| 694 | r.offset(-loop->getCelOrigin()); |
| 695 | auto tilesPts = m_dstGrid.tilesInCanvasRegion(gfx::Region(r)); |
| 696 | forEachTilePos( |
| 697 | loop, tilesPts, |
| 698 | [loop, r](const doc::ImageRef existentTileImage, |
| 699 | const gfx::Point tilePos) { |
| 700 | existentTileImage->copy( |
| 701 | loop->getDstImage(), |
| 702 | gfx::Clip(r.x - tilePos.x, |
| 703 | r.y - tilePos.y, r)); |
| 704 | }); |
| 705 | |
| 706 | if (tilesPts.size() > 1) { |
| 707 | forEachTilePos( |
| 708 | loop, tilesPts, |
| 709 | [loop](const doc::ImageRef existentTileImage, |
| 710 | const gfx::Point tilePos) { |
| 711 | loop->getDstImage()->copy( |
| 712 | existentTileImage.get(), |
| 713 | gfx::Clip(tilePos.x, tilePos.y, 0, 0, |
| 714 | existentTileImage->width(), |
| 715 | existentTileImage->height())); |
| 716 | }); |
| 717 | } |
| 718 | } |
| 719 | |
| 720 | // Loops over the points in tilesPts, and for each one calls the provided |
| 721 | // processTempTileImage callback passing to it the corresponding temp tile |
| 722 | // image and canvas position. |
| 723 | void forEachTilePos(ToolLoop* loop, |
| 724 | const std::vector<gfx::Point>& tilesPts, |
| 725 | const std::function<void(const doc::ImageRef existentTileImage, |
| 726 | const gfx::Point tilePos)>& processTempTileImage) { |
| 727 | ASSERT(loop->getCel()); |
| 728 | if (!loop->getCel()) |
| 729 | return; |
| 730 | |
| 731 | const Image* tilemapImage = loop->getCel()->image(); |
| 732 | |
| 733 | // Offset to convert a tile from dstImage coordinates to tilemap |
| 734 | // image coordinates (to get the tile from the original tilemap) |
| 735 | const gfx::Point tilePt0 = m_celGrid.canvasToTile(gfx::Point(0, 0)); |
| 736 | |
| 737 | for (const gfx::Point& tilePt : tilesPts) { |
| 738 | const gfx::Point tilePtInTilemap = tilePt0 + tilePt; |
| 739 | |
| 740 | // Ignore modifications outside the tilemap |
| 741 | if (!tilemapImage->bounds().contains(tilePtInTilemap)) |
| 742 | continue; |
| 743 | |
| 744 | const doc::tile_t t = tilemapImage->getPixel(tilePtInTilemap.x, |
| 745 | tilePtInTilemap.y); |
| 746 | if (t == doc::notile) |
| 747 | continue; |
| 748 | |
| 749 | const doc::tile_index ti = doc::tile_geti(t); |
| 750 | const doc::ImageRef existentTileImage = m_tempTileset->get(ti); |
| 751 | if (!existentTileImage) |
| 752 | continue; |
| 753 | |
| 754 | const gfx::Point tilePosInDstImage = m_dstGrid.tileToCanvas(tilePt); |
| 755 | processTempTileImage(existentTileImage, tilePosInDstImage); |
| 756 | } |
| 757 | } |
| 758 | |
| 759 | }; |
| 760 | |
| 761 | } // namespace tools |
| 762 | } // namespace app |
| 763 | |