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 | |