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/commands/cmd_flip.h"
13
14#include "app/app.h"
15#include "app/cmd/flip_mask.h"
16#include "app/cmd/flip_masked_cel.h"
17#include "app/cmd/set_cel_bounds.h"
18#include "app/cmd/set_mask_position.h"
19#include "app/cmd/trim_cel.h"
20#include "app/commands/params.h"
21#include "app/context_access.h"
22#include "app/doc_api.h"
23#include "app/doc_range.h"
24#include "app/i18n/strings.h"
25#include "app/modules/editors.h"
26#include "app/modules/gui.h"
27#include "app/tools/tool_box.h"
28#include "app/tx.h"
29#include "app/ui/editor/editor.h"
30#include "app/ui/editor/moving_pixels_state.h"
31#include "app/ui/status_bar.h"
32#include "app/ui/timeline/timeline.h"
33#include "app/ui/toolbar.h"
34#include "app/util/expand_cel_canvas.h"
35#include "app/util/range_utils.h"
36#include "doc/algorithm/flip_image.h"
37#include "doc/cel.h"
38#include "doc/cels_range.h"
39#include "doc/image.h"
40#include "doc/layer.h"
41#include "doc/mask.h"
42#include "doc/sprite.h"
43#include "fmt/format.h"
44#include "gfx/size.h"
45
46
47namespace app {
48
49FlipCommand::FlipCommand()
50 : Command(CommandId::Flip(), CmdRecordableFlag)
51{
52 m_flipMask = false;
53 m_flipType = doc::algorithm::FlipHorizontal;
54}
55
56void FlipCommand::onLoadParams(const Params& params)
57{
58 std::string target = params.get("target");
59 m_flipMask = (target == "mask");
60
61 std::string orientation = params.get("orientation");
62 m_flipType = (orientation == "vertical" ? doc::algorithm::FlipVertical:
63 doc::algorithm::FlipHorizontal);
64}
65
66bool FlipCommand::onEnabled(Context* ctx)
67{
68 return ctx->checkFlags(ContextFlags::ActiveDocumentIsWritable);
69}
70
71void FlipCommand::onExecute(Context* ctx)
72{
73 Site site = ctx->activeSite();
74 LockTimelineRange lockRange(App::instance()->timeline());
75
76 CelList cels;
77 if (m_flipMask) {
78#ifdef ENABLE_UI
79 // If we want to flip the visible mask we can go to
80 // MovingPixelsState (even when the range is enabled, because now
81 // PixelsMovement support ranges).
82 if (site.document()->isMaskVisible() &&
83 ctx->isUIAvailable()) {
84 // Select marquee tool
85 if (tools::Tool* tool = App::instance()->toolBox()
86 ->getToolById(tools::WellKnownTools::RectangularMarquee)) {
87 ToolBar::instance()->selectTool(tool);
88 current_editor->startFlipTransformation(m_flipType);
89 return;
90 }
91 }
92#endif
93
94 auto range = site.range();
95 if (range.enabled()) {
96 cels = get_unique_cels_to_edit_pixels(site.sprite(), range);
97 }
98 else if (site.cel() &&
99 site.layer() &&
100 site.layer()->canEditPixels()) {
101 cels.push_back(site.cel());
102 }
103
104 if (cels.empty()) {
105#ifdef ENABLE_UI
106 if (ctx->isUIAvailable()) {
107 StatusBar::instance()->showTip(
108 1000, Strings::statusbar_tips_all_layers_are_locked());
109 }
110#endif // ENABLE_UI
111 return;
112 }
113 }
114 // Flip the whole sprite (even locked layers)
115 else {
116 for (Cel* cel : site.sprite()->uniqueCels())
117 cels.push_back(cel);
118 }
119
120 ContextWriter writer(ctx);
121 Doc* document = writer.document();
122 Sprite* sprite = writer.sprite();
123 Tx tx(ctx, friendlyName());
124 DocApi api = document->getApi(tx);
125
126 Mask* mask = document->mask();
127 if (m_flipMask && document->isMaskVisible()) {
128 Site site = *writer.site();
129
130 for (Cel* cel : cels) {
131 // TODO add support to flip masked part of a reference layer
132 if (cel->layer()->isReference())
133 continue;
134
135 site.frame(cel->frame());
136 site.layer(cel->layer());
137
138 int x, y;
139 Image* image = site.image(&x, &y);
140 if (!image)
141 continue;
142
143 // When the mask is inside the cel, we can try to flip the
144 // pixels inside the image.
145 if (cel->bounds().contains(mask->bounds())) {
146 gfx::Rect flipBounds = mask->bounds();
147 flipBounds.offset(-x, -y);
148 flipBounds &= image->bounds();
149 if (flipBounds.isEmpty())
150 continue;
151
152 if (mask->bitmap() && !mask->isRectangular())
153 tx(new cmd::FlipMaskedCel(cel, m_flipType));
154 else
155 api.flipImage(image, flipBounds, m_flipType);
156
157 if (site.shouldTrimCel(cel))
158 tx(new cmd::TrimCel(cel));
159 }
160 // When the mask is bigger than the cel bounds, we have to
161 // expand the cel, make the flip, and shrink it again.
162 else {
163 gfx::Rect flipBounds = (sprite->bounds() & mask->bounds());
164 if (flipBounds.isEmpty())
165 continue;
166
167 ExpandCelCanvas expand(
168 site, cel->layer(),
169 TiledMode::NONE, tx,
170 ExpandCelCanvas::None);
171
172 expand.validateDestCanvas(gfx::Region(flipBounds));
173
174 if (mask->bitmap() && !mask->isRectangular())
175 doc::algorithm::flip_image_with_mask(
176 expand.getDestCanvas(), mask, m_flipType,
177 document->bgColor(cel->layer()));
178 else
179 doc::algorithm::flip_image(
180 expand.getDestCanvas(),
181 flipBounds, m_flipType);
182
183 expand.commit();
184 }
185 }
186 }
187 else {
188 for (Cel* cel : cels) {
189 Image* image = cel->image();
190
191 // Flip reference layer cel
192 if (cel->layer()->isReference()) {
193 gfx::RectF bounds = cel->boundsF();
194
195 if (m_flipType == doc::algorithm::FlipHorizontal)
196 bounds.x = sprite->width() - bounds.w - bounds.x;
197 if (m_flipType == doc::algorithm::FlipVertical)
198 bounds.y = sprite->height() - bounds.h - bounds.y;
199
200 tx(new cmd::SetCelBoundsF(cel, bounds));
201 }
202 else {
203 api.setCelPosition
204 (sprite, cel,
205 (m_flipType == doc::algorithm::FlipHorizontal ?
206 sprite->width() - image->width() - cel->x():
207 cel->x()),
208 (m_flipType == doc::algorithm::FlipVertical ?
209 sprite->height() - image->height() - cel->y():
210 cel->y()));
211 }
212
213 api.flipImage(image, image->bounds(), m_flipType);
214 }
215 }
216
217 // Flip the mask.
218 Image* maskBitmap = mask->bitmap();
219 if (maskBitmap) {
220 tx(new cmd::FlipMask(document, m_flipType));
221
222 // Flip the mask position because the
223 if (!m_flipMask)
224 tx(
225 new cmd::SetMaskPosition(
226 document,
227 gfx::Point(
228 (m_flipType == doc::algorithm::FlipHorizontal ?
229 sprite->width() - mask->bounds().x2():
230 mask->bounds().x),
231 (m_flipType == doc::algorithm::FlipVertical ?
232 sprite->height() - mask->bounds().y2():
233 mask->bounds().y))));
234 }
235
236 tx.commit();
237
238#ifdef ENABLE_UI
239 if (ctx->isUIAvailable())
240 update_screen_for_document(document);
241#endif
242}
243
244std::string FlipCommand::onGetFriendlyName() const
245{
246 std::string content;
247 std::string orientation;
248
249 if (m_flipMask)
250 content = Strings::commands_Flip_Selection();
251 else
252 content = Strings::commands_Flip_Canvas();
253
254 if (m_flipType == doc::algorithm::FlipHorizontal)
255 orientation = Strings::commands_Flip_Horizontally();
256 else
257 orientation = Strings::commands_Flip_Vertically();
258
259 return fmt::format(getBaseFriendlyName(), content, orientation);
260}
261
262Command* CommandFactory::createFlipCommand()
263{
264 return new FlipCommand;
265}
266
267} // namespace app
268