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/app.h"
13#include "app/cmd/set_cel_bounds.h"
14#include "app/commands/cmd_rotate.h"
15#include "app/commands/params.h"
16#include "app/context_access.h"
17#include "app/doc_api.h"
18#include "app/doc_range.h"
19#include "app/i18n/strings.h"
20#include "app/modules/editors.h"
21#include "app/modules/gui.h"
22#include "app/sprite_job.h"
23#include "app/tools/tool_box.h"
24#include "app/tx.h"
25#include "app/ui/color_bar.h"
26#include "app/ui/editor/editor.h"
27#include "app/ui/status_bar.h"
28#include "app/ui/timeline/timeline.h"
29#include "app/ui/toolbar.h"
30#include "app/util/range_utils.h"
31#include "base/convert_to.h"
32#include "doc/cel.h"
33#include "doc/cels_range.h"
34#include "doc/image.h"
35#include "doc/mask.h"
36#include "doc/sprite.h"
37#include "fmt/format.h"
38#include "ui/ui.h"
39
40namespace app {
41
42class RotateJob : public SpriteJob {
43 int m_angle;
44 CelList m_cels;
45 bool m_rotateSprite;
46
47public:
48
49 RotateJob(const ContextReader& reader,
50 const std::string& jobName,
51 int angle, const CelList& cels, bool rotateSprite)
52 : SpriteJob(reader, jobName.c_str())
53 , m_cels(cels)
54 , m_rotateSprite(rotateSprite) {
55 m_angle = angle;
56 }
57
58protected:
59
60 template<typename T>
61 void rotate_rect(gfx::RectT<T>& newBounds) {
62 const gfx::RectT<T> bounds = newBounds;
63 switch (m_angle) {
64 case 180:
65 newBounds.x = sprite()->width() - bounds.x - bounds.w;
66 newBounds.y = sprite()->height() - bounds.y - bounds.h;
67 break;
68 case 90:
69 newBounds.x = sprite()->height() - bounds.y - bounds.h;
70 newBounds.y = bounds.x;
71 newBounds.w = bounds.h;
72 newBounds.h = bounds.w;
73 break;
74 case -90:
75 newBounds.x = bounds.y;
76 newBounds.y = sprite()->width() - bounds.x - bounds.w;
77 newBounds.w = bounds.h;
78 newBounds.h = bounds.w;
79 break;
80 }
81 }
82
83 // [working thread]
84 void onJob() override {
85 DocApi api = document()->getApi(tx());
86
87 // 1) Rotate cel positions
88 for (Cel* cel : m_cels) {
89 Image* image = cel->image();
90 if (!image)
91 continue;
92
93 if (cel->layer()->isReference()) {
94 gfx::RectF bounds = cel->boundsF();
95 rotate_rect(bounds);
96 if (cel->boundsF() != bounds)
97 tx()(new cmd::SetCelBoundsF(cel, bounds));
98 }
99 else {
100 gfx::Rect bounds = cel->bounds();
101 rotate_rect(bounds);
102 if (bounds.origin() != cel->bounds().origin())
103 api.setCelPosition(sprite(), cel, bounds.x, bounds.y);
104 }
105 }
106
107 // 2) Rotate images
108 int i = 0;
109 for (Cel* cel : m_cels) {
110 Image* image = cel->image();
111 if (image) {
112 ImageRef new_image(Image::create(image->pixelFormat(),
113 m_angle == 180 ? image->width(): image->height(),
114 m_angle == 180 ? image->height(): image->width()));
115 new_image->setMaskColor(image->maskColor());
116
117 doc::rotate_image(image, new_image.get(), m_angle);
118 api.replaceImage(sprite(), cel->imageRef(), new_image);
119 }
120
121 jobProgress((float)i / m_cels.size());
122 ++i;
123
124 // cancel all the operation?
125 if (isCanceled())
126 return; // Tx destructor will undo all operations
127 }
128
129 // rotate mask
130 if (document()->isMaskVisible()) {
131 Mask* origMask = document()->mask();
132 std::unique_ptr<Mask> new_mask(new Mask());
133 const gfx::Rect& origBounds = origMask->bounds();
134 int x = 0, y = 0;
135
136 switch (m_angle) {
137 case 180:
138 x = sprite()->width() - origBounds.x - origBounds.w;
139 y = sprite()->height() - origBounds.y - origBounds.h;
140 break;
141 case 90:
142 x = sprite()->height() - origBounds.y - origBounds.h;
143 y = origBounds.x;
144 break;
145 case -90:
146 x = origBounds.y;
147 y = sprite()->width() - origBounds.x - origBounds.w;
148 break;
149 }
150
151 // create the new rotated mask
152 new_mask->replace(
153 gfx::Rect(x, y,
154 m_angle == 180 ? origBounds.w: origBounds.h,
155 m_angle == 180 ? origBounds.h: origBounds.w));
156 doc::rotate_image(origMask->bitmap(), new_mask->bitmap(), m_angle);
157
158 // Copy new mask
159 api.copyToCurrentMask(new_mask.get());
160 }
161
162 // change the sprite's size
163 if (m_rotateSprite && m_angle != 180)
164 api.setSpriteSize(sprite(), sprite()->height(), sprite()->width());
165 }
166
167};
168
169RotateCommand::RotateCommand()
170 : Command(CommandId::Rotate(), CmdRecordableFlag)
171{
172 m_flipMask = false;
173 m_angle = 0;
174}
175
176void RotateCommand::onLoadParams(const Params& params)
177{
178 std::string target = params.get("target");
179 m_flipMask = (target == "mask");
180
181 if (params.has_param("angle")) {
182 m_angle = strtol(params.get("angle").c_str(), NULL, 10);
183 }
184}
185
186bool RotateCommand::onEnabled(Context* context)
187{
188 return context->checkFlags(ContextFlags::ActiveDocumentIsWritable |
189 ContextFlags::HasActiveSprite);
190}
191
192void RotateCommand::onExecute(Context* context)
193{
194 {
195 Site site = context->activeSite();
196 CelList cels;
197 bool rotateSprite = false;
198
199 Timeline* timeline = App::instance()->timeline();
200 LockTimelineRange lockRange(timeline);
201
202 // Flip the mask or current cel
203 if (m_flipMask) {
204 // If we want to rotate the visible mask, we can go to
205 // MovingPixelsState (even when the range is enabled, because
206 // now PixelsMovement support ranges).
207 if (site.document()->isMaskVisible()) {
208 // Select marquee tool
209 if (tools::Tool* tool = App::instance()->toolBox()
210 ->getToolById(tools::WellKnownTools::RectangularMarquee)) {
211 ToolBar::instance()->selectTool(tool);
212 current_editor->startSelectionTransformation(gfx::Point(0, 0), m_angle);
213 return;
214 }
215 }
216
217 auto range = App::instance()->timeline()->range();
218 if (range.enabled())
219 cels = get_unique_cels_to_edit_pixels(site.sprite(), range);
220 else if (site.cel() &&
221 site.layer() &&
222 site.layer()->canEditPixels()) {
223 cels.push_back(site.cel());
224 }
225
226 if (cels.empty()) {
227 StatusBar::instance()->showTip(
228 1000, Strings::statusbar_tips_all_layers_are_locked());
229 return;
230 }
231 }
232 // Flip the whole sprite (even locked layers)
233 else if (site.sprite()) {
234 for (Cel* cel : site.sprite()->uniqueCels())
235 cels.push_back(cel);
236
237 rotateSprite = true;
238 }
239
240 ContextReader reader(context);
241 {
242 RotateJob job(reader, friendlyName(), m_angle, cels, rotateSprite);
243 job.startJob();
244 job.waitJob();
245 }
246 update_screen_for_document(reader.document());
247 }
248}
249
250std::string RotateCommand::onGetFriendlyName() const
251{
252 std::string content;
253 if (m_flipMask)
254 content = Strings::commands_Rotate_Selection();
255 else
256 content = Strings::commands_Rotate_Sprite();
257 return fmt::format(getBaseFriendlyName(),
258 content, base::convert_to<std::string>(m_angle));
259}
260
261Command* CommandFactory::createRotateCommand()
262{
263 return new RotateCommand;
264}
265
266} // namespace app
267