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 | |
40 | namespace app { |
41 | |
42 | class RotateJob : public SpriteJob { |
43 | int m_angle; |
44 | CelList m_cels; |
45 | bool m_rotateSprite; |
46 | |
47 | public: |
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 | |
58 | protected: |
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 | |
169 | RotateCommand::RotateCommand() |
170 | : Command(CommandId::Rotate(), CmdRecordableFlag) |
171 | { |
172 | m_flipMask = false; |
173 | m_angle = 0; |
174 | } |
175 | |
176 | void 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 | |
186 | bool RotateCommand::onEnabled(Context* context) |
187 | { |
188 | return context->checkFlags(ContextFlags::ActiveDocumentIsWritable | |
189 | ContextFlags::HasActiveSprite); |
190 | } |
191 | |
192 | void 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 | |
250 | std::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 | |
261 | Command* CommandFactory::createRotateCommand() |
262 | { |
263 | return new RotateCommand; |
264 | } |
265 | |
266 | } // namespace app |
267 | |