1// Aseprite
2// Copyright (C) 2019-2021 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#ifdef HAVE_CONFIG_H
9#include "config.h"
10#endif
11
12#include "app/ui/editor/pixels_movement.h"
13
14#include "app/app.h"
15#include "app/cmd/clear_mask.h"
16#include "app/cmd/deselect_mask.h"
17#include "app/cmd/set_mask.h"
18#include "app/cmd/trim_cel.h"
19#include "app/console.h"
20#include "app/doc.h"
21#include "app/doc_api.h"
22#include "app/i18n/strings.h"
23#include "app/modules/gui.h"
24#include "app/pref/preferences.h"
25#include "app/site.h"
26#include "app/snap_to_grid.h"
27#include "app/ui/editor/pivot_helpers.h"
28#include "app/ui/editor/vec2.h"
29#include "app/ui/status_bar.h"
30#include "app/ui_context.h"
31#include "app/util/cel_ops.h"
32#include "app/util/expand_cel_canvas.h"
33#include "app/util/new_image_from_mask.h"
34#include "app/util/range_utils.h"
35#include "base/pi.h"
36#include "doc/algorithm/flip_image.h"
37#include "doc/algorithm/rotate.h"
38#include "doc/algorithm/rotsprite.h"
39#include "doc/algorithm/shift_image.h"
40#include "doc/blend_internals.h"
41#include "doc/cel.h"
42#include "doc/image.h"
43#include "doc/layer.h"
44#include "doc/mask.h"
45#include "doc/sprite.h"
46#include "gfx/region.h"
47#include "render/render.h"
48
49#include <algorithm>
50
51#if _DEBUG
52#define DUMP_INNER_CMDS() dumpInnerCmds()
53#else
54#define DUMP_INNER_CMDS()
55#endif
56
57namespace app {
58
59PixelsMovement::InnerCmd::InnerCmd(InnerCmd&& c)
60 : type(None)
61{
62 std::swap(type, c.type);
63 std::swap(data, c.data);
64}
65
66PixelsMovement::InnerCmd::~InnerCmd()
67{
68 if (type == InnerCmd::Stamp)
69 delete data.stamp.transformation;
70}
71
72// static
73PixelsMovement::InnerCmd
74PixelsMovement::InnerCmd::MakeClear()
75{
76 InnerCmd c;
77 c.type = InnerCmd::Clear;
78 return c;
79}
80
81// static
82PixelsMovement::InnerCmd
83PixelsMovement::InnerCmd::MakeFlip(const doc::algorithm::FlipType flipType)
84{
85 InnerCmd c;
86 c.type = InnerCmd::Flip;
87 c.data.flip.type = flipType;
88 return c;
89}
90
91// static
92PixelsMovement::InnerCmd
93PixelsMovement::InnerCmd::MakeShift(const int dx, const int dy, const double angle)
94{
95 InnerCmd c;
96 c.type = InnerCmd::Shift;
97 c.data.shift.dx = dx;
98 c.data.shift.dy = dy;
99 c.data.shift.angle = angle;
100 return c;
101}
102
103// static
104PixelsMovement::InnerCmd
105PixelsMovement::InnerCmd::MakeStamp(const Transformation& t)
106{
107 InnerCmd c;
108 c.type = InnerCmd::Stamp;
109 c.data.stamp.transformation = new Transformation(t);
110 return c;
111}
112
113PixelsMovement::PixelsMovement(
114 Context* context,
115 Site site,
116 const Image* moveThis,
117 const Mask* mask,
118 const char* operationName)
119 : m_reader(context)
120 , m_site(site)
121 , m_document(site.document())
122 , m_tx(context, operationName)
123 , m_isDragging(false)
124 , m_adjustPivot(false)
125 , m_handle(NoHandle)
126 , m_originalImage(Image::createCopy(moveThis))
127 , m_opaque(false)
128 , m_maskColor(m_site.sprite()->transparentColor())
129 , m_canHandleFrameChange(false)
130 , m_fastMode(false)
131 , m_needsRotSpriteRedraw(false)
132{
133 double cornerThick = (m_site.tilemapMode() == TilemapMode::Tiles) ?
134 CORNER_THICK_FOR_TILEMAP_MODE :
135 CORNER_THICK_FOR_PIXELS_MODE;
136 Transformation transform(mask->bounds(), cornerThick);
137 set_pivot_from_preferences(transform);
138
139 m_initialData = transform;
140 m_currentData = transform;
141
142 m_initialMask.reset(new Mask(*mask));
143 m_initialMask0.reset(new Mask(*mask));
144 m_currentMask.reset(new Mask(*mask));
145
146 m_pivotVisConn =
147 Preferences::instance().selection.pivotVisibility.AfterChange.connect(
148 [this]{ onPivotChange(); });
149 m_pivotPosConn =
150 Preferences::instance().selection.pivotPosition.AfterChange.connect(
151 [this]{ onPivotChange(); });
152 m_rotAlgoConn =
153 Preferences::instance().selection.rotationAlgorithm.AfterChange.connect(
154 [this]{ onRotationAlgorithmChange(); });
155
156 // The extra cel must be null, because if it's not null, it means
157 // that someone else is using it (e.g. the editor brush preview),
158 // and its owner could destroy our new "extra cel".
159 ASSERT(!m_document->extraCel());
160 redrawExtraImage();
161 redrawCurrentMask();
162
163 // If the mask is different than the mask from the document
164 // (e.g. it's from Paste command), we've to replace the document
165 // mask and generate its boundaries.
166 if (mask != m_document->mask()) {
167 // Update document mask
168 m_tx(new cmd::SetMask(m_document, m_currentMask.get()));
169 m_document->generateMaskBoundaries(m_currentMask.get());
170 update_screen_for_document(m_document);
171 }
172}
173
174bool PixelsMovement::editMultipleCels() const
175{
176 return
177 (m_site.range().enabled() &&
178 (Preferences::instance().selection.multicelWhenLayersOrFrames() ||
179 m_site.range().type() == DocRange::kCels));
180}
181
182void PixelsMovement::setDelegate(PixelsMovementDelegate* delegate)
183{
184 m_delegate = delegate;
185}
186
187void PixelsMovement::setFastMode(const bool fastMode)
188{
189 bool redraw = (m_fastMode && !fastMode);
190 m_fastMode = fastMode;
191 if (m_needsRotSpriteRedraw && redraw) {
192 redrawExtraImage();
193 update_screen_for_document(m_document);
194 m_needsRotSpriteRedraw = false;
195 }
196}
197
198void PixelsMovement::flipImage(doc::algorithm::FlipType flipType)
199{
200 m_innerCmds.push_back(InnerCmd::MakeFlip(flipType));
201
202 flipOriginalImage(flipType);
203
204 {
205 ContextWriter writer(m_reader, 1000);
206
207 // Regenerate the transformed (rotated, scaled, etc.) image and
208 // mask.
209 redrawExtraImage();
210 redrawCurrentMask();
211 updateDocumentMask();
212
213 update_screen_for_document(m_document);
214 }
215}
216
217void PixelsMovement::rotate(double angle)
218{
219 ContextWriter writer(m_reader, 1000);
220 m_currentData.angle(
221 base::fmod_radians(
222 m_currentData.angle() + PI * -angle / 180.0));
223
224 m_document->setTransformation(m_currentData);
225
226 redrawExtraImage();
227 redrawCurrentMask();
228 updateDocumentMask();
229
230 update_screen_for_document(m_document);
231}
232
233void PixelsMovement::shift(int dx, int dy)
234{
235 const double angle = m_currentData.angle();
236 m_innerCmds.push_back(InnerCmd::MakeShift(dx, dy, angle));
237 shiftOriginalImage(dx, dy, angle);
238
239 {
240 ContextWriter writer(m_reader, 1000);
241
242 redrawExtraImage();
243 redrawCurrentMask();
244 updateDocumentMask();
245
246 update_screen_for_document(m_document);
247 }
248}
249
250void PixelsMovement::setTransformation(const Transformation& t)
251{
252 m_initialData = m_currentData;
253
254 setTransformationBase(t);
255
256 redrawCurrentMask();
257 updateDocumentMask();
258
259 update_screen_for_document(m_document);
260}
261
262void PixelsMovement::setTransformationBase(const Transformation& t)
263{
264 // Get old transformed corners, update transformation, and get new
265 // transformed corners. These corners will be used to know what to
266 // update in the editor's canvas.
267 auto oldCorners = m_currentData.transformedCorners();
268 m_currentData = t;
269 auto newCorners = m_currentData.transformedCorners();
270
271 redrawExtraImage();
272
273 m_document->setTransformation(m_currentData);
274
275 // Create a union of all corners, and that will be the bounds to
276 // redraw of the sprite.
277 gfx::Rect fullBounds;
278 for (int i=0; i<Transformation::Corners::NUM_OF_CORNERS; ++i) {
279 fullBounds |= gfx::Rect((int)oldCorners[i].x, (int)oldCorners[i].y, 1, 1);
280 fullBounds |= gfx::Rect((int)newCorners[i].x, (int)newCorners[i].y, 1, 1);
281 }
282
283 // If "fullBounds" is empty is because the cel was not moved
284 if (!fullBounds.isEmpty()) {
285 // Notify the modified region.
286 m_document->notifySpritePixelsModified(
287 m_site.sprite(),
288 gfx::Region(fullBounds),
289 m_site.frame());
290 }
291}
292
293void PixelsMovement::trim()
294{
295 ContextWriter writer(m_reader, 1000);
296 Cel* activeCel = m_site.cel();
297 bool restoreMask = false;
298
299 // TODO this is similar to clear_mask_from_cels()
300
301 for (Cel* cel : getEditableCels()) {
302 if (cel != activeCel) {
303 if (!restoreMask) {
304 m_document->setMask(m_initialMask0.get());
305 restoreMask = true;
306 }
307 m_tx(new cmd::ClearMask(cel));
308 }
309 // Current cel (m_site.cel()) can be nullptr when we paste in an
310 // empty cel (Ctrl+V) and cut (Ctrl+X) the floating pixels.
311 if (cel &&
312 cel->layer()->isTransparent()) {
313 m_tx(new cmd::TrimCel(cel));
314 }
315 }
316
317 if (restoreMask)
318 updateDocumentMask();
319}
320
321void PixelsMovement::cutMask()
322{
323 m_innerCmds.push_back(InnerCmd::MakeClear());
324
325 {
326 ContextWriter writer(m_reader, 1000);
327 if (writer.cel()) {
328 clear_mask_from_cel(m_tx,
329 writer.cel(),
330 m_site.tilemapMode(),
331 m_site.tilesetMode());
332
333 // Do not trim here so we don't lost the information about all
334 // linked cels related to "writer.cel()"
335 }
336 }
337
338 copyMask();
339}
340
341void PixelsMovement::copyMask()
342{
343 hideDocumentMask();
344}
345
346void PixelsMovement::catchImage(const gfx::PointF& pos, HandleType handle)
347{
348 ASSERT(handle != NoHandle);
349
350 m_catchPos = pos;
351 m_isDragging = true;
352 m_handle = handle;
353}
354
355void PixelsMovement::catchImageAgain(const gfx::PointF& pos, HandleType handle)
356{
357 // Create a new Transaction to move the pixels to other position
358 m_initialData = m_currentData;
359 m_isDragging = true;
360 m_catchPos = pos;
361 m_handle = handle;
362
363 hideDocumentMask();
364}
365
366void PixelsMovement::moveImage(const gfx::PointF& pos, MoveModifier moveModifier)
367{
368 ContextWriter writer(m_reader, 1000);
369 gfx::RectF bounds = m_initialData.bounds();
370 gfx::PointF abs_initial_pivot = m_initialData.pivot();
371 gfx::PointF abs_pivot = m_currentData.pivot();
372
373 auto newTransformation = m_currentData;
374
375 switch (m_handle) {
376
377 case MovePixelsHandle: {
378 double dx = (pos.x - m_catchPos.x);
379 double dy = (pos.y - m_catchPos.y);
380 if ((moveModifier & FineControl) == 0) {
381 if (dx >= 0.0) { dx = std::floor(dx); } else { dx = std::ceil(dx); }
382 if (dy >= 0.0) { dy = std::floor(dy); } else { dy = std::ceil(dy); }
383 }
384
385 if ((moveModifier & LockAxisMovement) == LockAxisMovement) {
386 if (std::abs(dx) < std::abs(dy))
387 dx = 0.0;
388 else
389 dy = 0.0;
390 }
391
392 bounds.offset(dx, dy);
393
394 if ((m_site.tilemapMode() == TilemapMode::Tiles) ||
395 (moveModifier & SnapToGridMovement) == SnapToGridMovement) {
396 // Snap the x1,y1 point to the grid.
397 gfx::Rect gridBounds = m_site.gridBounds();
398 gfx::PointF gridOffset(
399 snap_to_grid(
400 gridBounds,
401 gfx::Point(bounds.origin()),
402 PreferSnapTo::ClosestGridVertex));
403
404 // Now we calculate the difference from x1,y1 point and we can
405 // use it to adjust all coordinates (x1, y1, x2, y2).
406 bounds.setOrigin(gridOffset);
407 }
408
409 newTransformation.bounds(bounds);
410 newTransformation.pivot(abs_initial_pivot +
411 bounds.origin() -
412 m_initialData.bounds().origin());
413 break;
414 }
415
416 case ScaleNWHandle:
417 case ScaleNHandle:
418 case ScaleNEHandle:
419 case ScaleWHandle:
420 case ScaleEHandle:
421 case ScaleSWHandle:
422 case ScaleSHandle:
423 case ScaleSEHandle: {
424 static double handles[][2] = {
425 { 0.0, 0.0 }, { 0.5, 0.0 }, { 1.0, 0.0 },
426 { 0.0, 0.5 }, { 1.0, 0.5 },
427 { 0.0, 1.0 }, { 0.5, 1.0 }, { 1.0, 1.0 }
428 };
429 gfx::PointF pivot;
430 gfx::PointF handle(
431 handles[m_handle-ScaleNWHandle][0],
432 handles[m_handle-ScaleNWHandle][1]);
433
434 if ((moveModifier & ScaleFromPivot) == ScaleFromPivot) {
435 pivot = m_currentData.pivot();
436 }
437 else {
438 pivot.x = 1.0 - handle.x;
439 pivot.y = 1.0 - handle.y;
440 pivot.x = bounds.x + bounds.w*pivot.x;
441 pivot.y = bounds.y + bounds.h*pivot.y;
442 }
443
444 gfx::PointF a = bounds.origin();
445 gfx::PointF b = bounds.point2();
446
447 if ((moveModifier & MaintainAspectRatioMovement) == MaintainAspectRatioMovement) {
448 vec2 u = to_vec2(m_catchPos - pivot);
449 vec2 v = to_vec2(pos - pivot);
450 vec2 w = v.projectOn(u);
451 double scale = u.magnitude();
452 if (scale != 0.0) {
453 scale = (std::fabs(w.angle()-u.angle()) < PI/2.0 ? 1.0: -1.0) * w.magnitude() / scale;
454 }
455 else
456 scale = 1.0;
457
458 a.x = ((a.x-pivot.x)*scale + pivot.x);
459 a.y = ((a.y-pivot.y)*scale + pivot.y);
460 b.x = ((b.x-pivot.x)*scale + pivot.x);
461 b.y = ((b.y-pivot.y)*scale + pivot.y);
462 }
463 else {
464 handle.x = bounds.x + bounds.w*handle.x;
465 handle.y = bounds.y + bounds.h*handle.y;
466
467 double z = m_currentData.angle();
468 double w = (handle.x-pivot.x);
469 double h = (handle.y-pivot.y);
470 double dx = ((pos.x - m_catchPos.x) * std::cos(z) +
471 (pos.y - m_catchPos.y) * -std::sin(z));
472 double dy = ((pos.x - m_catchPos.x) * std::sin(z) +
473 (pos.y - m_catchPos.y) * std::cos(z));
474 if ((moveModifier & FineControl) == 0) {
475 if (dx >= 0.0) { dx = std::floor(dx); } else { dx = std::ceil(dx); }
476 if (dy >= 0.0) { dy = std::floor(dy); } else { dy = std::ceil(dy); }
477 }
478
479 if (m_handle == ScaleNHandle || m_handle == ScaleSHandle) {
480 dx = 0.0;
481 w = 1.0; // Any value != 0.0 to avoid div by zero
482 }
483 else if (m_handle == ScaleWHandle || m_handle == ScaleEHandle) {
484 dy = 0.0;
485 h = 1.0;
486 }
487
488 a.x = ((a.x-pivot.x)*(1.0+dx/w) + pivot.x);
489 a.y = ((a.y-pivot.y)*(1.0+dy/h) + pivot.y);
490 b.x = ((b.x-pivot.x)*(1.0+dx/w) + pivot.x);
491 b.y = ((b.y-pivot.y)*(1.0+dy/h) + pivot.y);
492 }
493
494 // Snap to grid when resizing tilemaps
495 if (m_site.tilemapMode() == TilemapMode::Tiles) {
496 gfx::Rect gridBounds = m_site.gridBounds();
497 a = gfx::PointF(snap_to_grid(gridBounds, gfx::Point(a), PreferSnapTo::BoxOrigin));
498 b = gfx::PointF(snap_to_grid(gridBounds, gfx::Point(b), PreferSnapTo::BoxOrigin));
499 }
500
501 // Do not use "gfx::Rect(a, b)" here because if a > b we want to
502 // keep a rectangle with negative width or height (to know that
503 // it was flipped).
504 bounds.x = a.x;
505 bounds.y = a.y;
506 bounds.w = b.x - a.x;
507 bounds.h = b.y - a.y;
508
509 newTransformation.bounds(bounds);
510 m_adjustPivot = true;
511 break;
512 }
513
514 case RotateNWHandle:
515 case RotateNEHandle:
516 case RotateSWHandle:
517 case RotateSEHandle: {
518 // Cannot rotate tiles
519 // TODO add support to rotate tiles in straight angles (changing tile flags)
520 if (m_site.tilemapMode() == TilemapMode::Tiles)
521 break;
522
523 double da = (std::atan2((double)(-pos.y + abs_pivot.y),
524 (double)(+pos.x - abs_pivot.x)) -
525 std::atan2((double)(-m_catchPos.y + abs_initial_pivot.y),
526 (double)(+m_catchPos.x - abs_initial_pivot.x)));
527 double newAngle = m_initialData.angle() + da;
528 newAngle = base::fmod_radians(newAngle);
529
530 // Is the "angle snap" is activated, we've to snap the angle
531 // to common (pixel art) angles.
532 if ((moveModifier & AngleSnapMovement) == AngleSnapMovement) {
533 // TODO make this configurable
534 static const double keyAngles[] = {
535 0.0, 26.565, 45.0, 63.435, 90.0, 116.565, 135.0, 153.435, 180.0,
536 180.0, -153.435, -135.0, -116, -90.0, -63.435, -45.0, -26.565
537 };
538
539 double newAngleDegrees = 180.0 * newAngle / PI;
540
541 int closest = 0;
542 int last = sizeof(keyAngles) / sizeof(keyAngles[0]) - 1;
543 for (int i=0; i<=last; ++i) {
544 if (std::fabs(newAngleDegrees-keyAngles[closest]) >
545 std::fabs(newAngleDegrees-keyAngles[i]))
546 closest = i;
547 }
548
549 newAngle = PI * keyAngles[closest] / 180.0;
550 }
551
552 newTransformation.angle(newAngle);
553 break;
554 }
555
556 case SkewNHandle:
557 case SkewSHandle:
558 case SkewWHandle:
559 case SkewEHandle: {
560 // Cannot skew tiles
561 // TODO could we support to skew tiles if we have the set of tiles (e.g. diagonals)?
562 // maybe too complex to implement in UI terms
563 if (m_site.tilemapMode() == TilemapMode::Tiles)
564 break;
565
566 // u
567 // ------>
568 //
569 // A --- B |
570 // | | | v
571 // | | |
572 // C --- D v
573 auto corners = m_initialData.transformedCorners();
574 auto A = corners[Transformation::Corners::LEFT_TOP];
575 auto B = corners[Transformation::Corners::RIGHT_TOP];
576 auto C = corners[Transformation::Corners::LEFT_BOTTOM];
577 auto D = corners[Transformation::Corners::RIGHT_BOTTOM];
578
579 // Pivot in pixels
580 gfx::PointF pivotPoint = m_currentData.pivot();
581
582 // Pivot in [0.0, 1.0] range
583 gfx::PointF pivot((pivotPoint.x - bounds.x) / ABS(bounds.w),
584 (pivotPoint.y - bounds.y) / ABS(bounds.h));
585
586 // Vector from AB (or CD), and AC (or BD)
587 vec2 u = to_vec2(B - A);
588 vec2 v = to_vec2(C - A);
589
590 // Move sides depending of a delta value (the mouse pos - catch
591 // pos) projected on u or v vectors. North and south cases are
592 // simple because only AB or CD sides can be modified (and then
593 // skew angle is calculated from the pivot position), but with
594 // east and west handles we modify all points to recalculate all
595 // the transformation parameters from scratch.
596 vec2 delta = to_vec2(pos - m_catchPos);
597 switch (m_handle) {
598 case SkewNHandle:
599 delta = delta.projectOn(u);
600 A.x += delta.x;
601 A.y += delta.y;
602 B.x += delta.x;
603 B.y += delta.y;
604 break;
605 case SkewSHandle:
606 delta = delta.projectOn(u);
607 C.x += delta.x;
608 C.y += delta.y;
609 D.x += delta.x;
610 D.y += delta.y;
611 break;
612 case SkewWHandle: {
613 delta = delta.projectOn(v);
614 A.x += delta.x;
615 A.y += delta.y;
616 C.x += delta.x;
617 C.y += delta.y;
618
619 vec2 toPivot = to_vec2(pivotPoint - (A*(1.0-pivot.y) + C*pivot.y));
620 vec2 toOtherSide = toPivot / (std::fabs(pivot.x) > 0.00001 ? pivot.x: 1.0);
621 B = A + to_point(toOtherSide);
622 D = C + to_point(toOtherSide);
623 break;
624 }
625 case SkewEHandle: {
626 delta = delta.projectOn(v);
627 B.x += delta.x;
628 B.y += delta.y;
629 D.x += delta.x;
630 D.y += delta.y;
631
632 vec2 toPivot = to_vec2(pivotPoint - (B*(1.0-pivot.y) + D*pivot.y));
633 vec2 toOtherSide = toPivot / (std::fabs(1.0-pivot.x) > 0.00001 ? (1.0-pivot.x): 1.0);
634 A = B + to_point(toOtherSide);
635 C = D + to_point(toOtherSide);
636 break;
637 }
638 }
639
640 // t0 will be a transformation without skew, so we can compare
641 // the angle between vector PR with skew and without skew.
642 auto t0 = m_initialData;
643 t0.skew(0.0);
644 auto corners0 = t0.transformedCorners();
645 auto A0 = corners0[Transformation::Corners::LEFT_TOP];
646 auto C0 = corners0[Transformation::Corners::LEFT_BOTTOM];
647
648 // A0 ------- B
649 // /| /
650 // / ACp / <- pivot position
651 // / | /
652 // C -C0----- D
653 vec2 AC0 = to_vec2(C0 - A0);
654 auto ACp = A0*(1.0-pivot.y) + C0*pivot.y;
655 vec2 AC;
656 switch (m_handle) {
657 case SkewNHandle: AC = to_vec2(ACp - A); break;
658 case SkewSHandle: AC = to_vec2(C - ACp); break;
659 case SkewWHandle:
660 case SkewEHandle: {
661 vec2 AB = to_vec2(B - A);
662 bounds.w = AB.magnitude();
663 bounds.x = pivotPoint.x - bounds.w*pivot.x;
664
665 // New rotation angle is the angle between AB points
666 newTransformation.angle(-AB.angle());
667
668 // New skew angle is the angle between AC0 (vector from A to
669 // B rotated 45 degrees, like an AC vector without skew) and
670 // the current to AC vector.
671 //
672 // B
673 // / |
674 // / |
675 // / |
676 // A |
677 // | \ D
678 // | \ /
679 // | / <- AC0=AB rotated 45 degrees, if pivot is here
680 // | /
681 // C
682 auto ABp = A*(1.0-pivot.x) + B*pivot.x;
683 AC0 = vec2(ABp.y - B.y, B.x - ABp.x);
684 AC = to_vec2(C - A);
685
686 bounds.h = AC.projectOn(AC0).magnitude();
687 bounds.y = pivotPoint.y - bounds.h*pivot.y;
688 newTransformation.bounds(bounds);
689 break;
690 }
691 }
692
693 // Calculate angle between AC and AC0
694 double newSkew = std::atan2(AC.x*AC0.y - AC.y*AC0.x, AC * AC0);
695 newSkew = std::clamp(newSkew, -PI*85.0/180.0, PI*85.0/180.0);
696 newTransformation.skew(newSkew);
697 break;
698 }
699
700 case PivotHandle: {
701 // Calculate the new position of the pivot
702 gfx::PointF newPivot = m_initialData.pivot() + pos - m_catchPos;
703 newTransformation = m_initialData;
704 newTransformation.displacePivotTo(newPivot);
705 break;
706 }
707 }
708
709 setTransformationBase(newTransformation);
710}
711
712void PixelsMovement::getDraggedImageCopy(std::unique_ptr<Image>& outputImage,
713 std::unique_ptr<Mask>& outputMask)
714{
715 gfx::Rect bounds = m_currentData.transformedBounds();
716 if (bounds.isEmpty())
717 return;
718
719 doc::PixelFormat pixelFormat;
720 gfx::Size imgSize;
721 if (m_site.tilemapMode() == TilemapMode::Tiles) {
722 imgSize = m_site.grid().canvasToTile(bounds).size();
723 pixelFormat = IMAGE_TILEMAP;
724 }
725 else {
726 imgSize = bounds.size();
727 pixelFormat = m_site.sprite()->pixelFormat();
728 }
729
730 std::unique_ptr<Image> image(
731 Image::create(
732 pixelFormat,
733 imgSize.w,
734 imgSize.h));
735
736 drawImage(m_currentData, image.get(),
737 gfx::PointF(bounds.origin()), false);
738
739 // Draw mask without shrinking it, so the mask size is equal to the
740 // "image" render.
741 std::unique_ptr<Mask> mask(new Mask);
742 drawMask(mask.get(), false);
743
744 // Now we can shrink and crop the image.
745 gfx::Rect oldMaskBounds = mask->bounds();
746 mask->shrink();
747 gfx::Rect newMaskBounds = mask->bounds();
748 if (newMaskBounds != oldMaskBounds) {
749 newMaskBounds.x -= oldMaskBounds.x;
750 newMaskBounds.y -= oldMaskBounds.y;
751 image.reset(crop_image(image.get(),
752 newMaskBounds.x,
753 newMaskBounds.y,
754 newMaskBounds.w,
755 newMaskBounds.h, 0));
756 }
757
758 outputImage.reset(image.release());
759 outputMask.reset(mask.release());
760}
761
762void PixelsMovement::stampImage()
763{
764 stampImage(false);
765 m_innerCmds.push_back(InnerCmd::MakeStamp(m_currentData));
766}
767
768// finalStamp: true if we have to stamp the current transformation
769// (m_currentData) in all cels of the active range, or false if we
770// have to stamp the image only in the current cel.
771void PixelsMovement::stampImage(bool finalStamp)
772{
773 ContextWriter writer(m_reader, 1000);
774 Cel* currentCel = m_site.cel();
775
776 CelList cels;
777 if (finalStamp) {
778 cels = getEditableCels();
779 }
780 // Current cel (m_site.cel()) can be nullptr when we paste in an
781 // empty cel (Ctrl+V) and cut (Ctrl+X) the floating pixels.
782 else {
783 cels.push_back(currentCel);
784 }
785
786 if (currentCel && currentCel->layer() &&
787 currentCel->layer()->isImage() &&
788 !currentCel->layer()->isEditableHierarchy()) {
789 Transformation initialCelPos(gfx::Rect(m_initialMask0->bounds()), m_currentData.cornerThick());
790 redrawExtraImage(&initialCelPos);
791 stampExtraCelImage();
792 }
793
794 for (Cel* target : cels) {
795 // We'll re-create the transformation for the other cels
796 if (target != currentCel) {
797 ASSERT(target);
798 m_site.layer(target->layer());
799 m_site.frame(target->frame());
800 ASSERT(m_site.cel() == target);
801
802 reproduceAllTransformationsWithInnerCmds();
803 }
804
805 redrawExtraImage();
806 stampExtraCelImage();
807 }
808
809 currentCel = m_site.cel();
810 if (currentCel &&
811 (m_site.layer() != currentCel->layer() ||
812 m_site.frame() != currentCel->frame())) {
813 m_site.layer(currentCel->layer());
814 m_site.frame(currentCel->frame());
815 redrawExtraImage();
816 }
817}
818
819void PixelsMovement::stampExtraCelImage()
820{
821 const Image* image = m_extraCel->image();
822 if (!image)
823 return;
824
825 const Cel* cel = m_extraCel->cel();
826
827 // Expand the canvas to paste the image in the fully visible
828 // portion of sprite.
829 ExpandCelCanvas expand(
830 m_site, m_site.layer(),
831 TiledMode::NONE, m_tx,
832 ExpandCelCanvas::None);
833
834 gfx::Point dstPt;
835 gfx::Size canvasImageSize = image->size();
836 if (m_site.tilemapMode() == TilemapMode::Tiles) {
837 doc::Grid grid = m_site.grid();
838 dstPt = grid.canvasToTile(cel->position());
839 canvasImageSize = grid.tileToCanvas(gfx::Rect(dstPt, canvasImageSize)).size();
840 }
841 else {
842 dstPt = cel->position() - expand.getCel()->position();
843 }
844
845 // We cannot use cel->bounds() because cel->image() is nullptr
846 expand.validateDestCanvas(
847 gfx::Region(gfx::Rect(cel->position(), canvasImageSize)));
848
849 expand.getDestCanvas()->copy(image, gfx::Clip(dstPt, image->bounds()));
850
851 expand.commit();
852}
853
854void PixelsMovement::dropImageTemporarily()
855{
856 m_isDragging = false;
857
858 {
859 ContextWriter writer(m_reader, 1000);
860
861 // TODO Add undo information so the user can undo each transformation step.
862
863 // Displace the pivot to the new site:
864 if (m_adjustPivot) {
865 m_adjustPivot = false;
866 adjustPivot();
867
868 if (m_delegate)
869 m_delegate->onPivotChange();
870 }
871
872 redrawCurrentMask();
873 updateDocumentMask();
874
875 update_screen_for_document(m_document);
876 }
877}
878
879void PixelsMovement::adjustPivot()
880{
881 // Get the a factor for the X/Y position of the initial pivot
882 // position inside the initial non-rotated bounds.
883 gfx::PointF pivotPosFactor(m_initialData.pivot() - m_initialData.bounds().origin());
884 pivotPosFactor.x /= m_initialData.bounds().w;
885 pivotPosFactor.y /= m_initialData.bounds().h;
886
887 // Get the current transformed bounds.
888 auto corners = m_currentData.transformedCorners();
889
890 // The new pivot will be located from the rotated left-top
891 // corner a distance equal to the transformed bounds's
892 // width/height multiplied with the previously calculated X/Y
893 // factor.
894 vec2 newPivot(corners.leftTop().x,
895 corners.leftTop().y);
896 newPivot += pivotPosFactor.x * to_vec2(corners.rightTop() - corners.leftTop());
897 newPivot += pivotPosFactor.y * to_vec2(corners.leftBottom() - corners.leftTop());
898
899 m_currentData.displacePivotTo(gfx::PointF(newPivot.x, newPivot.y));
900}
901
902void PixelsMovement::dropImage()
903{
904 m_isDragging = false;
905
906 // Stamp the image in the current layer.
907 stampImage(true);
908
909 // Put the new mask
910 m_document->setMask(m_initialMask0.get());
911 m_tx(new cmd::SetMask(m_document, m_currentMask.get()));
912
913 // This is the end of the whole undo transaction.
914 m_tx.commit();
915
916 // Destroy the extra cel (this cel will be used by the drawing
917 // cursor surely).
918 ContextWriter writer(m_reader, 1000);
919 m_document->setExtraCel(ExtraCelRef(nullptr));
920}
921
922void PixelsMovement::discardImage(const CommitChangesOption commit,
923 const KeepMaskOption keepMask)
924{
925 m_isDragging = false;
926
927 // Deselect the mask (here we don't stamp the image)
928 m_document->setMask(m_initialMask0.get());
929 if (keepMask == DontKeepMask) {
930 m_tx(new cmd::DeselectMask(m_document));
931 }
932 else {
933 m_tx(new cmd::SetMask(m_document, m_currentMask.get()));
934 }
935
936 if (commit == CommitChanges)
937 m_tx.commit();
938
939 // Destroy the extra cel and regenerate the mask boundaries (we've
940 // just deselect the mask).
941 ContextWriter writer(m_reader, 1000);
942 m_document->setExtraCel(ExtraCelRef(nullptr));
943 m_document->generateMaskBoundaries();
944}
945
946bool PixelsMovement::isDragging() const
947{
948 return m_isDragging;
949}
950
951gfx::Rect PixelsMovement::getImageBounds()
952{
953 const Cel* cel = m_extraCel->cel();
954 const Image* image = m_extraCel->image();
955
956 ASSERT(cel != NULL);
957 ASSERT(image != NULL);
958
959 return gfx::Rect(cel->x(), cel->y(), image->width(), image->height());
960}
961
962gfx::Size PixelsMovement::getInitialImageSize() const
963{
964 return gfx::Size(m_originalImage->width(),
965 m_originalImage->height());
966}
967
968void PixelsMovement::setMaskColor(bool opaque, color_t mask_color)
969{
970 ContextWriter writer(m_reader, 1000);
971 m_opaque = opaque;
972 m_maskColor = mask_color;
973 redrawExtraImage();
974
975 update_screen_for_document(m_document);
976}
977
978void PixelsMovement::redrawExtraImage(Transformation* transformation)
979{
980 if (!transformation)
981 transformation = &m_currentData;
982
983 int t, opacity = (m_site.layer()->isImage() ?
984 static_cast<LayerImage*>(m_site.layer())->opacity(): 255);
985 Cel* cel = m_site.cel();
986 if (cel) opacity = MUL_UN8(opacity, cel->opacity(), t);
987
988 if (!m_extraCel)
989 m_extraCel.reset(new ExtraCel);
990
991 gfx::Rect bounds = transformation->transformedBounds();
992
993 if (!bounds.isEmpty()) {
994 gfx::Size extraCelSize;
995 if (m_site.tilemapMode() == TilemapMode::Tiles) {
996 // Transforming tiles
997 extraCelSize = m_site.grid().canvasToTile(bounds).size();
998 }
999 else {
1000 // Transforming pixels
1001 extraCelSize = bounds.size();
1002 }
1003
1004 m_extraCel->create(
1005 m_site.tilemapMode(),
1006 m_document->sprite(),
1007 bounds,
1008 extraCelSize,
1009 m_site.frame(),
1010 opacity);
1011 m_extraCel->setType(render::ExtraType::PATCH);
1012 m_extraCel->setBlendMode(m_site.layer()->isImage() ?
1013 static_cast<LayerImage*>(m_site.layer())->blendMode():
1014 BlendMode::NORMAL);
1015 }
1016 else
1017 m_extraCel->reset();
1018
1019 m_document->setExtraCel(m_extraCel);
1020
1021 if (m_extraCel->image()) {
1022 // Draw the transformed pixels in the extra-cel which is the chunk
1023 // of pixels that the user is moving.
1024 drawImage(*transformation, m_extraCel->image(),
1025 gfx::PointF(bounds.origin()), true);
1026 }
1027}
1028
1029void PixelsMovement::redrawCurrentMask()
1030{
1031 drawMask(m_currentMask.get(), true);
1032}
1033
1034void PixelsMovement::drawImage(
1035 const Transformation& transformation,
1036 doc::Image* dst, const gfx::PointF& pt,
1037 const bool renderOriginalLayer)
1038{
1039 ASSERT(dst);
1040
1041 auto corners = transformation.transformedCorners();
1042 gfx::Rect bounds = corners.bounds(transformation.cornerThick());
1043
1044 if (m_site.tilemapMode() == TilemapMode::Tiles) {
1045 dst->setMaskColor(doc::notile);
1046 dst->clear(dst->maskColor());
1047
1048 if (renderOriginalLayer && m_site.cel()) {
1049 doc::Grid grid = m_site.grid();
1050 dst->copy(m_site.cel()->image(),
1051 gfx::Clip(0, 0, grid.canvasToTile(bounds)));
1052 }
1053
1054 drawTransformedTilemap(
1055 transformation,
1056 dst, m_originalImage.get(),
1057 m_initialMask.get());
1058 }
1059 else {
1060 dst->setMaskColor(m_site.sprite()->transparentColor());
1061 dst->clear(dst->maskColor());
1062
1063 if (renderOriginalLayer) {
1064 render::Render render;
1065 render.renderLayer(
1066 dst, m_site.layer(), m_site.frame(),
1067 gfx::Clip(bounds.x-pt.x, bounds.y-pt.y, bounds),
1068 BlendMode::SRC);
1069 }
1070
1071 color_t maskColor = m_maskColor;
1072
1073 // In case that Opaque option is enabled, or if we are drawing the
1074 // image for the clipboard (renderOriginalLayer is false), we use a
1075 // dummy mask color to call drawParallelogram(). In this way all
1076 // pixels will be opaqued (all colors are copied)
1077 if (m_opaque ||
1078 !renderOriginalLayer) {
1079 if (m_originalImage->pixelFormat() == IMAGE_INDEXED)
1080 maskColor = -1;
1081 else
1082 maskColor = 0;
1083 }
1084 m_originalImage->setMaskColor(maskColor);
1085
1086 drawParallelogram(
1087 transformation,
1088 dst, m_originalImage.get(),
1089 m_initialMask.get(), corners, pt);
1090 }
1091}
1092
1093void PixelsMovement::drawMask(doc::Mask* mask, bool shrink)
1094{
1095 auto corners = m_currentData.transformedCorners();
1096 gfx::Rect bounds = corners.bounds(m_currentData.cornerThick());
1097
1098 if (bounds.isEmpty()) {
1099 mask->clear();
1100 return;
1101 }
1102
1103 mask->replace(bounds);
1104 if (shrink)
1105 mask->freeze();
1106 clear_image(mask->bitmap(), 0);
1107 drawParallelogram(m_currentData,
1108 mask->bitmap(),
1109 m_initialMask->bitmap(),
1110 nullptr,
1111 corners,
1112 gfx::PointF(bounds.origin()));
1113 if (shrink)
1114 mask->unfreeze();
1115}
1116
1117void PixelsMovement::drawParallelogram(
1118 const Transformation& transformation,
1119 doc::Image* dst, const doc::Image* src, const doc::Mask* mask,
1120 const Transformation::Corners& corners,
1121 const gfx::PointF& leftTop)
1122{
1123 tools::RotationAlgorithm rotAlgo = Preferences::instance().selection.rotationAlgorithm();
1124
1125 // When the scale isn't modified and we have no rotation or a
1126 // right/straight-angle, we should use the fast rotation algorithm,
1127 // as it's pixel-perfect match with the original selection when just
1128 // a translation is applied.
1129 double angle = 180.0*transformation.angle()/PI;
1130 if (!Preferences::instance().selection.forceRotsprite() &&
1131 (std::fabs(std::fmod(std::fabs(angle), 90.0)) < 0.01 ||
1132 std::fabs(std::fmod(std::fabs(angle), 90.0)-90.0) < 0.01)) {
1133 rotAlgo = tools::RotationAlgorithm::FAST;
1134 }
1135
1136 // Don't use RotSprite if we are in "fast mode"
1137 if (rotAlgo == tools::RotationAlgorithm::ROTSPRITE && m_fastMode) {
1138 m_needsRotSpriteRedraw = true;
1139 rotAlgo = tools::RotationAlgorithm::FAST;
1140 }
1141
1142retry:; // In case that we don't have enough memory for RotSprite
1143 // we can try with the fast algorithm anyway.
1144
1145 switch (rotAlgo) {
1146
1147 case tools::RotationAlgorithm::FAST:
1148 doc::algorithm::parallelogram(
1149 dst, src, (mask ? mask->bitmap(): nullptr),
1150 int(corners.leftTop().x-leftTop.x),
1151 int(corners.leftTop().y-leftTop.y),
1152 int(corners.rightTop().x-leftTop.x),
1153 int(corners.rightTop().y-leftTop.y),
1154 int(corners.rightBottom().x-leftTop.x),
1155 int(corners.rightBottom().y-leftTop.y),
1156 int(corners.leftBottom().x-leftTop.x),
1157 int(corners.leftBottom().y-leftTop.y));
1158 break;
1159
1160 case tools::RotationAlgorithm::ROTSPRITE:
1161 try {
1162 doc::algorithm::rotsprite_image(
1163 dst, src, (mask ? mask->bitmap(): nullptr),
1164 int(corners.leftTop().x-leftTop.x),
1165 int(corners.leftTop().y-leftTop.y),
1166 int(corners.rightTop().x-leftTop.x),
1167 int(corners.rightTop().y-leftTop.y),
1168 int(corners.rightBottom().x-leftTop.x),
1169 int(corners.rightBottom().y-leftTop.y),
1170 int(corners.leftBottom().x-leftTop.x),
1171 int(corners.leftBottom().y-leftTop.y));
1172 }
1173 catch (const std::bad_alloc&) {
1174 StatusBar::instance()->showTip(
1175 1000,
1176 Strings::statusbar_tips_not_enough_rotsprite_memory());
1177
1178 rotAlgo = tools::RotationAlgorithm::FAST;
1179 goto retry;
1180 }
1181 break;
1182
1183 }
1184}
1185
1186static void merge_tilemaps(Image* dst, const Image* src, gfx::Clip area)
1187{
1188 if (!area.clip(dst->width(), dst->height(), src->width(), src->height()))
1189 return;
1190
1191 ImageConstIterator<TilemapTraits> src_it(src, area.srcBounds(), area.src.x, area.src.y);
1192 ImageIterator<TilemapTraits> dst_it(dst, area.dstBounds(), area.dst.x, area.dst.y);
1193
1194 for (int y=0; y<area.size.h; ++y) {
1195 for (int x=0; x<area.size.w; ++x) {
1196 if (*src_it != doc::notile)
1197 *dst_it = *src_it;
1198 ++src_it;
1199 ++dst_it;
1200 }
1201 }
1202}
1203
1204void PixelsMovement::drawTransformedTilemap(
1205 const Transformation& transformation,
1206 doc::Image* dst, const doc::Image* src, const doc::Mask* mask)
1207{
1208 ASSERT(dst->pixelFormat() == IMAGE_TILEMAP);
1209 ASSERT(src->pixelFormat() == IMAGE_TILEMAP);
1210
1211 const int boxw = std::max(1, src->width()-2);
1212 const int boxh = std::max(1, src->height()-2);
1213
1214 // Function to copy a whole row of tiles (h=number of tiles in Y axis)
1215 auto draw_row =
1216 [dst, src, boxw](int y, int v, int h) {
1217 merge_tilemaps(dst, src, gfx::Clip(0, y, 0, v, 1, h));
1218 if (boxw) {
1219 const int u = std::min(1, src->width()-1);
1220 for (int x=1; x<dst->width()-1; x+=boxw)
1221 merge_tilemaps(dst, src, gfx::Clip(x, y, u, v, boxw, h));
1222 }
1223 merge_tilemaps(dst, src, gfx::Clip(dst->width()-1, y, src->width()-1, v, 1, h));
1224 };
1225
1226 draw_row(0, 0, 1);
1227 if (boxh) {
1228 const int v = std::min(1, src->height()-1);
1229 for (int y=1; y<dst->height()-1; y+=boxh)
1230 draw_row(y, v, boxh);
1231 }
1232 draw_row(dst->height()-1, src->height()-1, 1);
1233}
1234
1235void PixelsMovement::onPivotChange()
1236{
1237 set_pivot_from_preferences(m_currentData);
1238 onRotationAlgorithmChange();
1239
1240 if (m_delegate)
1241 m_delegate->onPivotChange();
1242}
1243
1244void PixelsMovement::onRotationAlgorithmChange()
1245{
1246 try {
1247 redrawExtraImage();
1248 redrawCurrentMask();
1249 updateDocumentMask();
1250
1251 update_screen_for_document(m_document);
1252 }
1253 catch (const std::exception& ex) {
1254 Console::showException(ex);
1255 }
1256}
1257
1258void PixelsMovement::updateDocumentMask()
1259{
1260 m_document->setMask(m_currentMask.get());
1261 m_document->generateMaskBoundaries(m_currentMask.get());
1262}
1263
1264void PixelsMovement::hideDocumentMask()
1265{
1266 m_document->destroyMaskBoundaries();
1267 update_screen_for_document(m_document);
1268}
1269
1270void PixelsMovement::flipOriginalImage(const doc::algorithm::FlipType flipType)
1271{
1272 // Flip the image.
1273 doc::algorithm::flip_image(
1274 m_originalImage.get(),
1275 gfx::Rect(gfx::Point(0, 0),
1276 gfx::Size(m_originalImage->width(),
1277 m_originalImage->height())),
1278 flipType);
1279
1280 // Flip the mask.
1281 doc::algorithm::flip_image(
1282 m_initialMask->bitmap(),
1283 gfx::Rect(gfx::Point(0, 0), m_initialMask->bounds().size()),
1284 flipType);
1285}
1286
1287void PixelsMovement::shiftOriginalImage(const int dx, const int dy,
1288 const double angle)
1289{
1290 doc::algorithm::shift_image(
1291 m_originalImage.get(), dx, dy, angle);
1292}
1293
1294// Returns the list of cels that will be transformed (the first item
1295// in the list must be the current cel that was transformed if the cel
1296// wasn't nullptr).
1297CelList PixelsMovement::getEditableCels()
1298{
1299 CelList cels;
1300
1301 if (editMultipleCels()) {
1302 cels = get_unique_cels_to_edit_pixels(m_site.sprite(),
1303 m_site.range());
1304 }
1305 else {
1306 // TODO This case is used in paste too, where the cel() can be
1307 // nullptr (e.g. we paste the clipboard image into an empty
1308 // cel).
1309 if (m_site.layer() &&
1310 m_site.layer()->canEditPixels()) {
1311 cels.push_back(m_site.cel());
1312 }
1313 return cels;
1314 }
1315
1316 // Current cel (m_site.cel()) can be nullptr when we paste in an
1317 // empty cel (Ctrl+V) and cut (Ctrl+X) the floating pixels.
1318 if (m_site.cel() &&
1319 m_site.cel()->layer()->canEditPixels()) {
1320 CelList::iterator it;
1321
1322 // If we are in a linked cel, remove the cel that matches the
1323 // linked cel. In this way we avoid having two Cel in cels
1324 // pointing to the same CelData.
1325 if (Cel* link = m_site.cel()->link()) {
1326 it = std::find_if(cels.begin(), cels.end(),
1327 [link](const Cel* cel){
1328 return (cel == link ||
1329 cel->link() == link);
1330 });
1331 }
1332 else {
1333 it = std::find(cels.begin(), cels.end(), m_site.cel());
1334 }
1335 if (it != cels.end())
1336 cels.erase(it);
1337 cels.insert(cels.begin(), m_site.cel());
1338 }
1339
1340 return cels;
1341}
1342
1343bool PixelsMovement::gotoFrame(const doc::frame_t deltaFrame)
1344{
1345 if (editMultipleCels()) {
1346 Layer* layer = m_site.layer();
1347 ASSERT(layer);
1348
1349 const doc::SelectedFrames frames = m_site.range().selectedFrames();
1350 doc::frame_t initialFrame = m_site.frame();
1351 doc::frame_t frame = initialFrame + deltaFrame;
1352
1353 if (frames.size() >= 2) {
1354 for (; !frames.contains(frame) &&
1355 !layer->cel(frame); frame+=deltaFrame) {
1356 if (deltaFrame > 0 && frame > frames.lastFrame()) {
1357 frame = frames.firstFrame();
1358 break;
1359 }
1360 else if (deltaFrame < 0 && frame < frames.firstFrame()) {
1361 frame = frames.lastFrame();
1362 break;
1363 }
1364 }
1365
1366 if (frame == initialFrame ||
1367 !frames.contains(frame) ||
1368 // TODO At the moment we don't support going to an empty cel,
1369 // so we don't handle these cases
1370 !layer->cel(frame)) {
1371 return false;
1372 }
1373
1374 // Rollback all the actions, go to the next/previous frame and
1375 // reproduce all transformation again so the new frame is the
1376 // preview for the transformation.
1377 m_tx.rollbackAndStartAgain();
1378
1379 {
1380 m_canHandleFrameChange = true;
1381 {
1382 ContextWriter writer(m_reader, 1000);
1383 writer.context()->setActiveFrame(frame);
1384 m_site.frame(frame);
1385 }
1386 m_canHandleFrameChange = false;
1387 }
1388
1389 reproduceAllTransformationsWithInnerCmds();
1390 return true;
1391 }
1392 }
1393 return false;
1394}
1395
1396// Reproduces all the inner commands in the active m_site
1397void PixelsMovement::reproduceAllTransformationsWithInnerCmds()
1398{
1399 TRACEARGS("MOVPIXS: reproduceAllTransformationsWithInnerCmds",
1400 "layer", m_site.layer()->name(),
1401 "frame", m_site.frame());
1402 DUMP_INNER_CMDS();
1403
1404 m_document->setMask(m_initialMask0.get());
1405 m_initialMask->copyFrom(m_initialMask0.get());
1406 m_originalImage.reset(
1407 new_image_from_mask(
1408 m_site, m_initialMask.get(),
1409 Preferences::instance().experimental.newBlend()));
1410
1411 for (const InnerCmd& c : m_innerCmds) {
1412 switch (c.type) {
1413 case InnerCmd::Clear:
1414 clear_mask_from_cel(m_tx,
1415 m_site.cel(),
1416 m_site.tilemapMode(),
1417 m_site.tilesetMode());
1418 break;
1419 case InnerCmd::Flip:
1420 flipOriginalImage(c.data.flip.type);
1421 break;
1422 case InnerCmd::Shift:
1423 shiftOriginalImage(c.data.shift.dx,
1424 c.data.shift.dy,
1425 c.data.shift.angle);
1426 break;
1427 case InnerCmd::Stamp:
1428 redrawExtraImage(c.data.stamp.transformation);
1429 stampExtraCelImage();
1430 break;
1431 }
1432 }
1433
1434 redrawExtraImage();
1435 redrawCurrentMask();
1436 updateDocumentMask();
1437}
1438
1439#if _DEBUG
1440void PixelsMovement::dumpInnerCmds()
1441{
1442 TRACEARGS("MOVPIXS: InnerCmds size=", m_innerCmds.size());
1443 for (auto& c : m_innerCmds) {
1444 switch (c.type) {
1445 case InnerCmd::None:
1446 TRACEARGS("MOVPIXS: - None");
1447 break;
1448 case InnerCmd::Clear:
1449 TRACEARGS("MOVPIXS: - Clear");
1450 break;
1451 case InnerCmd::Flip:
1452 TRACEARGS("MOVPIXS: - Flip",
1453 (c.data.flip.type == doc::algorithm::FlipHorizontal ? "Horizontal":
1454 "Vertical"));
1455 break;
1456 case InnerCmd::Shift:
1457 TRACEARGS("MOVPIXS: - Shift",
1458 "dx=", c.data.shift.dx,
1459 "dy=", c.data.shift.dy,
1460 "angle=", c.data.shift.angle);
1461 break;
1462 case InnerCmd::Stamp:
1463 TRACEARGS("MOVPIXS: - Stamp",
1464 "angle=", c.data.stamp.transformation->angle(),
1465 "pivot=", c.data.stamp.transformation->pivot().x,
1466 c.data.stamp.transformation->pivot().y,
1467 "bounds=", c.data.stamp.transformation->bounds().x,
1468 c.data.stamp.transformation->bounds().y,
1469 c.data.stamp.transformation->bounds().w,
1470 c.data.stamp.transformation->bounds().h);
1471 break;
1472 }
1473 }
1474}
1475#endif // _DEBUG
1476
1477} // namespace app
1478