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
11namespace app {
12namespace tools {
13
14struct 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
24static 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
33class IntertwineNone : public Intertwine {
34public:
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
46class IntertwineFirstPoint : public Intertwine {
47public:
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
81class 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
95public:
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
181class IntertwineAsRectangles : public Intertwine {
182public:
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
276private:
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
295class IntertwineAsEllipses : public Intertwine {
296public:
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
390class IntertwineAsBezier : public Intertwine {
391public:
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
426class 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
460public:
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
591protected:
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
602private:
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