From 3e478d3efa745789cbc054c767947bc9c2a6c3e7 Mon Sep 17 00:00:00 2001 From: David Capello Date: Thu, 5 Sep 2019 15:03:13 -0300 Subject: [PATCH] Transform/scale/rotate multiple cels at the same time (fix #1148, #1172, #1238, #1364) Requested in twitter, the forum, and in several other places and frequently over all these years: https://community.aseprite.org/t/scaling-multiple-frames-simultaneously/240 --- src/app/cmd_transaction.cpp | 15 + src/app/cmd_transaction.h | 5 + src/app/commands/cmd_flip.cpp | 24 +- src/app/commands/cmd_rotate.cpp | 25 +- src/app/transaction.cpp | 13 +- src/app/transaction.h | 6 +- src/app/tx.h | 5 + src/app/ui/doc_view.cpp | 30 +- src/app/ui/editor/editor.cpp | 17 + src/app/ui/editor/editor.h | 7 + src/app/ui/editor/moving_pixels_state.cpp | 20 +- src/app/ui/editor/moving_pixels_state.h | 4 + src/app/ui/editor/pixels_movement.cpp | 522 +++++++++++++++++----- src/app/ui/editor/pixels_movement.h | 70 ++- src/app/ui/timeline/timeline.cpp | 15 +- src/app/ui/timeline/timeline.h | 4 +- src/app/util/clipboard.cpp | 49 +- src/app/util/clipboard.h | 9 +- 18 files changed, 659 insertions(+), 181 deletions(-) diff --git a/src/app/cmd_transaction.cpp b/src/app/cmd_transaction.cpp index d062736d0..949ad17ad 100644 --- a/src/app/cmd_transaction.cpp +++ b/src/app/cmd_transaction.cpp @@ -31,6 +31,21 @@ CmdTransaction::CmdTransaction(const std::string& label, { } +CmdTransaction* CmdTransaction::moveToEmptyCopy() +{ + CmdTransaction* copy = new CmdTransaction(m_label, + m_changeSavedState, + m_savedCounter); + copy->m_spritePositionBefore = m_spritePositionBefore; + copy->m_spritePositionAfter = m_spritePositionAfter; + if (m_ranges) { + copy->m_ranges.reset(new Ranges); + copy->m_ranges->m_before = std::move(m_ranges->m_before); + copy->m_ranges->m_after = std::move(m_ranges->m_after); + } + return copy; +} + void CmdTransaction::setNewDocRange(const DocRange& range) { #ifdef ENABLE_UI diff --git a/src/app/cmd_transaction.h b/src/app/cmd_transaction.h index 27f6e225e..1bed0b2a0 100644 --- a/src/app/cmd_transaction.h +++ b/src/app/cmd_transaction.h @@ -25,6 +25,11 @@ namespace app { CmdTransaction(const std::string& label, bool changeSavedState, int* savedCounter); + // Moves the CmdTransaction internals to a new copy in case that + // we want to rollback this CmdTransaction and start again with + // the new CmdTransaction. + CmdTransaction* moveToEmptyCopy(); + void setNewDocRange(const DocRange& range); void updateSpritePositionAfter(); diff --git a/src/app/commands/cmd_flip.cpp b/src/app/commands/cmd_flip.cpp index 082c71ecd..8d40b8147 100644 --- a/src/app/commands/cmd_flip.cpp +++ b/src/app/commands/cmd_flip.cpp @@ -76,6 +76,19 @@ void FlipCommand::onExecute(Context* context) CelList cels; if (m_flipMask) { + // If we want to flip the visible mask we can go to + // MovingPixelsState (even when the range is enabled, because now + // PixelsMovement support ranges). + if (site.document()->isMaskVisible()) { + // Select marquee tool + if (tools::Tool* tool = App::instance()->toolBox() + ->getToolById(tools::WellKnownTools::RectangularMarquee)) { + ToolBar::instance()->selectTool(tool); + current_editor->startFlipTransformation(m_flipType); + return; + } + } + auto range = timeline->range(); if (range.enabled()) { cels = get_unlocked_unique_cels(site.sprite(), range); @@ -83,17 +96,6 @@ void FlipCommand::onExecute(Context* context) else if (site.cel() && site.layer() && site.layer()->isEditable()) { - // If we want to flip the visible mask for the current cel, - // we can go to MovingPixelsState. - if (site.document()->isMaskVisible()) { - // Select marquee tool - if (tools::Tool* tool = App::instance()->toolBox() - ->getToolById(tools::WellKnownTools::RectangularMarquee)) { - ToolBar::instance()->selectTool(tool); - current_editor->startFlipTransformation(m_flipType); - return; - } - } cels.push_back(site.cel()); } diff --git a/src/app/commands/cmd_rotate.cpp b/src/app/commands/cmd_rotate.cpp index b280fac24..edd77ea9a 100644 --- a/src/app/commands/cmd_rotate.cpp +++ b/src/app/commands/cmd_rotate.cpp @@ -203,24 +203,25 @@ void RotateCommand::onExecute(Context* context) // Flip the mask or current cel if (m_flipMask) { + // If we want to rotate the visible mask, we can go to + // MovingPixelsState (even when the range is enabled, because + // now PixelsMovement support ranges). + if (site.document()->isMaskVisible()) { + // Select marquee tool + if (tools::Tool* tool = App::instance()->toolBox() + ->getToolById(tools::WellKnownTools::RectangularMarquee)) { + ToolBar::instance()->selectTool(tool); + current_editor->startSelectionTransformation(gfx::Point(0, 0), m_angle); + return; + } + } + auto range = App::instance()->timeline()->range(); if (range.enabled()) cels = get_unlocked_unique_cels(site.sprite(), range); else if (site.cel() && site.layer() && site.layer()->isEditable()) { - // If we want to rotate the visible mask for the current cel, - // we can go to MovingPixelsState. - if (site.document()->isMaskVisible()) { - // Select marquee tool - if (tools::Tool* tool = App::instance()->toolBox() - ->getToolById(tools::WellKnownTools::RectangularMarquee)) { - ToolBar::instance()->selectTool(tool); - current_editor->startSelectionTransformation(gfx::Point(0, 0), m_angle); - return; - } - } - cels.push_back(site.cel()); } diff --git a/src/app/transaction.cpp b/src/app/transaction.cpp index 4142cd7dd..da113c06e 100644 --- a/src/app/transaction.cpp +++ b/src/app/transaction.cpp @@ -58,7 +58,7 @@ Transaction::~Transaction() try { // If it isn't committed, we have to rollback all changes. if (m_cmds) - rollback(); + rollback(nullptr); } catch (...) { // Just avoid throwing an exception in the dtor (just in case @@ -96,7 +96,14 @@ void Transaction::commit() m_doc->generateMaskBoundaries(); } -void Transaction::rollback() +void Transaction::rollbackAndStartAgain() +{ + auto newCmds = m_cmds->moveToEmptyCopy(); + rollback(newCmds); + newCmds->execute(m_ctx); +} + +void Transaction::rollback(CmdTransaction* newCmds) { ASSERT(m_cmds); TX_TRACE("TX: Rollback <%s>\n", m_cmds->label().c_str()); @@ -104,7 +111,7 @@ void Transaction::rollback() m_cmds->undo(); delete m_cmds; - m_cmds = nullptr; + m_cmds = newCmds; } void Transaction::execute(Cmd* cmd) diff --git a/src/app/transaction.h b/src/app/transaction.h index 988d9737e..8f9807195 100644 --- a/src/app/transaction.h +++ b/src/app/transaction.h @@ -67,6 +67,10 @@ namespace app { // updates the Undo History window UI. void commit(); + // Discard everything that was added so far. We can start + // executing new Cmds again. + void rollbackAndStartAgain(); + void execute(Cmd* cmd); private: @@ -74,7 +78,7 @@ namespace app { enum class Changes { kNone = 0, kSelection = 1 }; - void rollback(); + void rollback(CmdTransaction* newCmds); // DocObserver impl void onSelectionChanged(DocEvent& ev) override; diff --git a/src/app/tx.h b/src/app/tx.h index 7c39b23a2..916efb8ab 100644 --- a/src/app/tx.h +++ b/src/app/tx.h @@ -1,4 +1,5 @@ // Aseprite +// Copyright (C) 2019 Igara Studio S.A. // Copyright (C) 2018 David Capello // // This program is distributed under the terms of @@ -47,6 +48,10 @@ namespace app { m_transaction->commit(); } + void rollbackAndStartAgain() { + m_transaction->rollbackAndStartAgain(); + } + void operator()(Cmd* cmd) { m_transaction->execute(cmd); } diff --git a/src/app/ui/doc_view.cpp b/src/app/ui/doc_view.cpp index fc2b57514..0859a809c 100644 --- a/src/app/ui/doc_view.cpp +++ b/src/app/ui/doc_view.cpp @@ -19,8 +19,8 @@ #include "app/commands/commands.h" #include "app/console.h" #include "app/context_access.h" -#include "app/doc_event.h" #include "app/doc_access.h" +#include "app/doc_event.h" #include "app/i18n/strings.h" #include "app/modules/editors.h" #include "app/modules/palettes.h" @@ -36,6 +36,7 @@ #include "app/ui/workspace.h" #include "app/ui_context.h" #include "app/util/clipboard.h" +#include "app/util/range_utils.h" #include "base/fs.h" #include "doc/layer.h" #include "doc/sprite.h" @@ -572,24 +573,29 @@ bool DocView::onClear(Context* ctx) // In other case we delete the mask or the cel. ContextWriter writer(ctx); - Doc* document = writer.document(); + Doc* document = site.document(); bool visibleMask = document->isMaskVisible(); - if (!writer.cel()) + CelList cels; + if (site.range().enabled()) { + cels = get_unlocked_unique_cels(site.sprite(), site.range()); + } + else if (site.cel()) { + cels.push_back(site.cel()); + } + + if (cels.empty()) // No cels to modify return false; { Tx tx(writer.context(), "Clear"); - tx(new cmd::ClearMask(writer.cel())); + const bool deselectMask = + (visibleMask && + !Preferences::instance().selection.keepSelectionAfterClear()); - // If the cel wasn't deleted by cmd::ClearMask, we trim it. - if (writer.cel() && - writer.cel()->layer()->isTransparent()) - tx(new cmd::TrimCel(writer.cel())); - - if (visibleMask && - !Preferences::instance().selection.keepSelectionAfterClear()) - tx(new cmd::DeselectMask(document)); + clipboard::clear_mask_from_cels( + tx, document, cels, + deselectMask); tx.commit(); } diff --git a/src/app/ui/editor/editor.cpp b/src/app/ui/editor/editor.cpp index e707323f2..72fea9c36 100644 --- a/src/app/ui/editor/editor.cpp +++ b/src/app/ui/editor/editor.cpp @@ -48,6 +48,7 @@ #include "app/ui/main_window.h" #include "app/ui/skin/skin_theme.h" #include "app/ui/status_bar.h" +#include "app/ui/timeline/timeline.h" #include "app/ui/toolbar.h" #include "app/ui_context.h" #include "base/bind.h" @@ -384,6 +385,13 @@ void Editor::getSite(Site* site) const getCurrentEditorInk()->isSlice()) { site->selectedSlices(m_selectedSlices); } + + // TODO we should not access timeline directly here + Timeline* timeline = App::instance()->timeline(); + if (timeline && + timeline->range().enabled()) { + site->range(timeline->range()); + } } Site Editor::getSite() const @@ -2182,6 +2190,15 @@ bool Editor::canStartMovingSelectionPixels() int(m_customizationDelegate->getPressedKeyAction(KeyContext::TranslatingSelection) & KeyAction::CopySelection))); } +bool Editor::keepTimelineRange() +{ + if (MovingPixelsState* movingPixels = dynamic_cast(m_state.get())) { + if (movingPixels->canHandleFrameChange()) + return true; + } + return false; +} + EditorHit Editor::calcHit(const gfx::Point& mouseScreenPos) { tools::Ink* ink = getCurrentEditorInk(); diff --git a/src/app/ui/editor/editor.h b/src/app/ui/editor/editor.h index 5274dddee..0c2ce3810 100644 --- a/src/app/ui/editor/editor.h +++ b/src/app/ui/editor/editor.h @@ -218,6 +218,13 @@ namespace app { // way to move the selection. bool canStartMovingSelectionPixels(); + // Returns true if the range selected in the timeline should be + // kept. E.g. When we are moving/transforming pixels on multiple + // cels, the MovingPixelsState can handle previous/next frame + // commands, so it's nice to keep the timeline range intact while + // we are in the MovingPixelsState. + bool keepTimelineRange(); + // Returns the element that will be modified if the mouse is used // in the given position. EditorHit calcHit(const gfx::Point& mouseScreenPos); diff --git a/src/app/ui/editor/moving_pixels_state.cpp b/src/app/ui/editor/moving_pixels_state.cpp index b8b75948b..6c63e319f 100644 --- a/src/app/ui/editor/moving_pixels_state.cpp +++ b/src/app/ui/editor/moving_pixels_state.cpp @@ -597,6 +597,22 @@ void MovingPixelsState::onBeforeCommandExecution(CommandExecutionEvent& ev) } } } + // We can use previous/next frames while transforming the selection + // to switch between frames + else if (command->id() == CommandId::GotoPreviousFrame() || + command->id() == CommandId::GotoPreviousFrameWithSameTag()) { + if (m_pixelsMovement->gotoFrame(-1)) { + ev.cancel(); + return; + } + } + else if (command->id() == CommandId::GotoNextFrame() || + command->id() == CommandId::GotoNextFrameWithSameTag()) { + if (m_pixelsMovement->gotoFrame(+1)) { + ev.cancel(); + return; + } + } if (m_pixelsMovement) dropPixels(); @@ -614,8 +630,10 @@ void MovingPixelsState::onBeforeFrameChanged(Editor* editor) if (!isActiveDocument()) return; - if (m_pixelsMovement) + if (m_pixelsMovement && + !m_pixelsMovement->canHandleFrameChange()) { dropPixels(); + } } void MovingPixelsState::onBeforeLayerChanged(Editor* editor) diff --git a/src/app/ui/editor/moving_pixels_state.h b/src/app/ui/editor/moving_pixels_state.h index 6c9f97305..2cf479fb5 100644 --- a/src/app/ui/editor/moving_pixels_state.h +++ b/src/app/ui/editor/moving_pixels_state.h @@ -33,6 +33,10 @@ namespace app { MovingPixelsState(Editor* editor, ui::MouseMessage* msg, PixelsMovementPtr pixelsMovement, HandleType handle); virtual ~MovingPixelsState(); + bool canHandleFrameChange() const { + return m_pixelsMovement->canHandleFrameChange(); + } + void translate(const gfx::Point& delta); void rotate(double angle); void flip(doc::algorithm::FlipType flipType); diff --git a/src/app/ui/editor/pixels_movement.cpp b/src/app/ui/editor/pixels_movement.cpp index 6af623aa9..5400f0c1f 100644 --- a/src/app/ui/editor/pixels_movement.cpp +++ b/src/app/ui/editor/pixels_movement.cpp @@ -27,6 +27,8 @@ #include "app/ui/status_bar.h" #include "app/ui_context.h" #include "app/util/expand_cel_canvas.h" +#include "app/util/new_image_from_mask.h" +#include "app/util/range_utils.h" #include "base/bind.h" #include "base/pi.h" #include "base/vector2d.h" @@ -43,6 +45,14 @@ #include "gfx/region.h" #include "render/render.h" +#include + +#if _DEBUG +#define DUMP_INNER_CMDS() dumpInnerCmds() +#else +#define DUMP_INNER_CMDS() +#endif + namespace app { template @@ -50,6 +60,60 @@ static inline const base::Vector2d point2Vector(const gfx::PointT& pt return base::Vector2d(pt.x, pt.y); } +PixelsMovement::InnerCmd::InnerCmd(InnerCmd&& c) + : type(None) +{ + std::swap(type, c.type); + std::swap(data, c.data); +} + +PixelsMovement::InnerCmd::~InnerCmd() +{ + if (type == InnerCmd::Stamp) + delete data.stamp.transformation; +} + +// static +PixelsMovement::InnerCmd +PixelsMovement::InnerCmd::MakeClear() +{ + InnerCmd c; + c.type = InnerCmd::Clear; + return c; +} + +// static +PixelsMovement::InnerCmd +PixelsMovement::InnerCmd::MakeFlip(const doc::algorithm::FlipType flipType) +{ + InnerCmd c; + c.type = InnerCmd::Flip; + c.data.flip.type = flipType; + return c; +} + +// static +PixelsMovement::InnerCmd +PixelsMovement::InnerCmd::MakeShift(const int dx, const int dy, const double angle) +{ + InnerCmd c; + c.type = InnerCmd::Shift; + c.data.shift.dx = dx; + c.data.shift.dy = dy; + c.data.shift.angle = angle; + return c; +} + +// static +PixelsMovement::InnerCmd +PixelsMovement::InnerCmd::MakeStamp(const Transformation& t) +{ + InnerCmd c; + c.type = InnerCmd::Stamp; + c.data.stamp.transformation = new Transformation(t); + return c; +} + PixelsMovement::PixelsMovement( Context* context, Site site, @@ -59,16 +123,15 @@ PixelsMovement::PixelsMovement( : m_reader(context) , m_site(site) , m_document(site.document()) - , m_sprite(site.sprite()) - , m_layer(site.layer()) , m_tx(context, operationName) - , m_setMaskCmd(nullptr) + // , m_setMaskCmd(nullptr) , m_isDragging(false) , m_adjustPivot(false) , m_handle(NoHandle) , m_originalImage(Image::createCopy(moveThis)) , m_opaque(false) - , m_maskColor(m_sprite->transparentColor()) + , m_maskColor(m_site.sprite()->transparentColor()) + , m_canHandleFrameChange(false) { Transformation transform(mask->bounds()); set_pivot_from_preferences(transform); @@ -76,8 +139,9 @@ PixelsMovement::PixelsMovement( m_initialData = transform; m_currentData = transform; - m_initialMask = new Mask(*mask); - m_currentMask = new Mask(*mask); + m_initialMask.reset(new Mask(*mask)); + m_initialMask0.reset(new Mask(*mask)); + m_currentMask.reset(new Mask(*mask)); m_pivotVisConn = Preferences::instance().selection.pivotVisibility.AfterChange.connect( @@ -96,57 +160,22 @@ PixelsMovement::PixelsMovement( redrawExtraImage(); redrawCurrentMask(); - // If the mask isn't in the document (e.g. it's from Paste command), - // we've to replace the document mask and generate its boundaries. - // - // This is really tricky. PixelsMovement is used in two situations: - // 1) When the current selection is transformed, and - // 2) when the user pastes the clipboard content. - // - // In the first case, the current document selection is used. And a - // cutMask() command could be called after PixelsMovement ctor. We - // need the following stack of Cmd instances in the Transaction: - // - cmd::ClearMask: clears the old mask) - // - cmd::SetMask (m_setMaskCmd): replaces the old mask with a new mask - // The new mask in m_setMaskCmd is replaced each time the mask is modified. - // - // In the second case, the mask isn't in the document, is a new mask - // used to paste the pixels, so we've to replace the document mask. - // The Transaction contains just a: - // - cmd::SetMask - // - // The main point here is that cmd::SetMask must be the last item in - // the Transaction using the mask (because we use cmd::SetMask::setNewMask()). - // - // TODO Simplify this code in some way or make explicit both usages + // If the mask is different than the mask from the document + // (e.g. it's from Paste command), we've to replace the document + // mask and generate its boundaries. if (mask != m_document->mask()) { - updateDocumentMask(); + // Update document mask + m_tx(new cmd::SetMask(m_document, m_currentMask.get())); + m_document->generateMaskBoundaries(m_currentMask.get()); update_screen_for_document(m_document); } } -PixelsMovement::~PixelsMovement() -{ - delete m_originalImage; - delete m_initialMask; - delete m_currentMask; -} - void PixelsMovement::flipImage(doc::algorithm::FlipType flipType) { - // Flip the image. - doc::algorithm::flip_image( - m_originalImage, - gfx::Rect(gfx::Point(0, 0), - gfx::Size(m_originalImage->width(), - m_originalImage->height())), - flipType); + m_innerCmds.push_back(InnerCmd::MakeFlip(flipType)); - // Flip the mask. - doc::algorithm::flip_image( - m_initialMask->bitmap(), - gfx::Rect(gfx::Point(0, 0), m_initialMask->bounds().size()), - flipType); + flipOriginalImage(flipType); { ContextWriter writer(m_reader, 1000); @@ -179,7 +208,10 @@ void PixelsMovement::rotate(double angle) void PixelsMovement::shift(int dx, int dy) { - doc::algorithm::shift_image(m_originalImage, dx, dy, m_currentData.angle()); + const double angle = m_currentData.angle(); + m_innerCmds.push_back(InnerCmd::MakeShift(dx, dy, angle)); + shiftOriginalImage(dx, dy, angle); + { ContextWriter writer(m_reader, 1000); @@ -194,16 +226,31 @@ void PixelsMovement::shift(int dx, int dy) void PixelsMovement::trim() { ContextWriter writer(m_reader, 1000); + Cel* activeCel = m_site.cel(); + bool restoreMask = false; - // writer.cel() can be nullptr when we paste in an empty cel - // (Ctrl+V) and cut (Ctrl+X) the floating pixels. - if (writer.cel() && - writer.cel()->layer()->isTransparent()) - m_tx(new cmd::TrimCel(writer.cel())); + // TODO this is similar to clear_mask_from_cels() + + for (Cel* cel : getEditableCels()) { + if (cel != activeCel) { + if (!restoreMask) { + m_document->setMask(m_initialMask0.get()); + restoreMask = true; + } + m_tx(new cmd::ClearMask(cel)); + } + if (cel->layer()->isTransparent()) + m_tx(new cmd::TrimCel(cel)); + } + + if (restoreMask) + updateDocumentMask(); } void PixelsMovement::cutMask() { + m_innerCmds.push_back(InnerCmd::MakeClear()); + { ContextWriter writer(m_reader, 1000); if (writer.cel()) { @@ -465,8 +512,10 @@ void PixelsMovement::moveImage(const gfx::Point& pos, MoveModifier moveModifier) // If "fullBounds" is empty is because the cel was not moved if (!fullBounds.isEmpty()) { // Notify the modified region. - m_document->notifySpritePixelsModified(m_sprite, gfx::Region(fullBounds), - m_site.frame()); + m_document->notifySpritePixelsModified( + m_site.sprite(), + gfx::Region(fullBounds), + m_site.frame()); } } @@ -474,9 +523,11 @@ void PixelsMovement::getDraggedImageCopy(std::unique_ptr& outputImage, std::unique_ptr& outputMask) { gfx::Rect bounds = m_currentData.transformedBounds(); - std::unique_ptr image(Image::create(m_sprite->pixelFormat(), bounds.w, bounds.h)); + std::unique_ptr image( + Image::create( + m_site.sprite()->pixelFormat(), bounds.w, bounds.h)); - drawImage(image.get(), bounds.origin(), false); + drawImage(m_currentData, image.get(), bounds.origin(), false); // Draw mask without shrinking it, so the mask size is equal to the // "image" render. @@ -502,41 +553,83 @@ void PixelsMovement::getDraggedImageCopy(std::unique_ptr& outputImage, } void PixelsMovement::stampImage() +{ + stampImage(false); + m_innerCmds.push_back(InnerCmd::MakeStamp(m_currentData)); +} + +// finalStamp: true if we have to stamp the current transformation +// (m_currentData) in all cels of the active range, or false if we +// have to stamp the image only in the current cel. +void PixelsMovement::stampImage(bool finalStamp) +{ + ContextWriter writer(m_reader, 1000); + Cel* currentCel = m_site.cel(); + + CelList cels; + if (finalStamp) { + cels = getEditableCels(); + } + // Current cel (m_site.cel()) can be nullptr when we paste in an + // empty cel (Ctrl+V) and cut (Ctrl+X) the floating pixels. + else { + cels.push_back(currentCel); + } + + for (Cel* target : cels) { + // We'll re-create the transformation for the other cels + if (target != currentCel) { + ASSERT(target); + m_site.layer(target->layer()); + m_site.frame(target->frame()); + ASSERT(m_site.cel() == target); + + reproduceAllTransformationsWithInnerCmds(); + } + + redrawExtraImage(); + stampExtraCelImage(); + } + + currentCel = m_site.cel(); + if (currentCel && + (m_site.layer() != currentCel->layer() || + m_site.frame() != currentCel->frame())) { + m_site.layer(currentCel->layer()); + m_site.frame(currentCel->frame()); + redrawExtraImage(); + } +} + +void PixelsMovement::stampExtraCelImage() { const Cel* cel = m_extraCel->cel(); const Image* image = m_extraCel->image(); - ASSERT(cel && image); + // Expand the canvas to paste the image in the fully visible + // portion of sprite. + ExpandCelCanvas expand( + m_site, m_site.layer(), + TiledMode::NONE, m_tx, + ExpandCelCanvas::None); - { - ContextWriter writer(m_reader, 1000); - { - // Expand the canvas to paste the image in the fully visible - // portion of sprite. - ExpandCelCanvas expand( - m_site, m_site.layer(), - TiledMode::NONE, m_tx, - ExpandCelCanvas::None); + // We cannot use cel->bounds() because cel->image() is nullptr + gfx::Rect modifiedRect( + cel->x(), + cel->y(), + image->width(), + image->height()); - // We cannot use cel->bounds() because cel->image() is nullptr - gfx::Rect modifiedRect( - cel->x(), - cel->y(), - image->width(), - image->height()); + gfx::Region modifiedRegion(modifiedRect); + expand.validateDestCanvas(modifiedRegion); - gfx::Region modifiedRegion(modifiedRect); - expand.validateDestCanvas(modifiedRegion); + expand.getDestCanvas()->copy( + image, gfx::Clip( + cel->x()-expand.getCel()->x(), + cel->y()-expand.getCel()->y(), + image->bounds())); - expand.getDestCanvas()->copy( - image, gfx::Clip( - cel->x()-expand.getCel()->x(), - cel->y()-expand.getCel()->y(), - image->bounds())); - - expand.commit(); - } - } + expand.commit(); } void PixelsMovement::dropImageTemporarily() @@ -586,7 +679,11 @@ void PixelsMovement::dropImage() m_isDragging = false; // Stamp the image in the current layer. - stampImage(); + stampImage(true); + + // Put the new mask + m_document->setMask(m_initialMask0.get()); + m_tx(new cmd::SetMask(m_document, m_currentMask.get())); // This is the end of the whole undo transaction. m_tx.commit(); @@ -603,8 +700,13 @@ void PixelsMovement::discardImage(const CommitChangesOption commit, m_isDragging = false; // Deselect the mask (here we don't stamp the image) - if (keepMask == DontKeepMask) + m_document->setMask(m_initialMask0.get()); + if (keepMask == DontKeepMask) { m_tx(new cmd::DeselectMask(m_document)); + } + else { + m_tx(new cmd::SetMask(m_document, m_currentMask.get())); + } if (commit == CommitChanges) m_tx.commit(); @@ -648,47 +750,56 @@ void PixelsMovement::setMaskColor(bool opaque, color_t mask_color) update_screen_for_document(m_document); } -void PixelsMovement::redrawExtraImage() +void PixelsMovement::redrawExtraImage(Transformation* transformation) { - int t, opacity = (m_layer->isImage() ? static_cast(m_layer)->opacity(): 255); + if (!transformation) + transformation = &m_currentData; + + int t, opacity = (m_site.layer()->isImage() ? + static_cast(m_site.layer())->opacity(): 255); Cel* cel = m_site.cel(); if (cel) opacity = MUL_UN8(opacity, cel->opacity(), t); - gfx::Rect bounds = m_currentData.transformedBounds(); + gfx::Rect bounds = transformation->transformedBounds(); + + if (!m_extraCel) + m_extraCel.reset(new ExtraCel); - m_extraCel.reset(new ExtraCel); m_extraCel->create(m_document->sprite(), bounds, m_site.frame(), opacity); m_extraCel->setType(render::ExtraType::PATCH); - m_extraCel->setBlendMode(m_layer->isImage() ? - static_cast(m_layer)->blendMode(): + m_extraCel->setBlendMode(m_site.layer()->isImage() ? + static_cast(m_site.layer())->blendMode(): BlendMode::NORMAL); m_document->setExtraCel(m_extraCel); // Draw the transformed pixels in the extra-cel which is the chunk // of pixels that the user is moving. - drawImage(m_extraCel->image(), bounds.origin(), true); + drawImage(*transformation, m_extraCel->image(), bounds.origin(), true); } void PixelsMovement::redrawCurrentMask() { - drawMask(m_currentMask, true); + drawMask(m_currentMask.get(), true); } -void PixelsMovement::drawImage(doc::Image* dst, const gfx::Point& pt, bool renderOriginalLayer) +void PixelsMovement::drawImage( + const Transformation& transformation, + doc::Image* dst, const gfx::Point& pt, + const bool renderOriginalLayer) { ASSERT(dst); Transformation::Corners corners; - m_currentData.transformBox(corners); + transformation.transformBox(corners); gfx::Rect bounds = corners.bounds(); - dst->setMaskColor(m_sprite->transparentColor()); + dst->setMaskColor(m_site.sprite()->transparentColor()); dst->clear(dst->maskColor()); if (renderOriginalLayer) { render::Render render; render.renderLayer( - dst, m_layer, m_site.frame(), + dst, m_site.layer(), m_site.frame(), gfx::Clip(bounds.x-pt.x, bounds.y-pt.y, bounds), BlendMode::SRC); } @@ -708,7 +819,10 @@ void PixelsMovement::drawImage(doc::Image* dst, const gfx::Point& pt, bool rende } m_originalImage->setMaskColor(maskColor); - drawParallelogram(dst, m_originalImage, m_initialMask, corners, pt); + drawParallelogram( + transformation, + dst, m_originalImage.get(), + m_initialMask.get(), corners, pt); } void PixelsMovement::drawMask(doc::Mask* mask, bool shrink) @@ -721,7 +835,8 @@ void PixelsMovement::drawMask(doc::Mask* mask, bool shrink) if (shrink) mask->freeze(); clear_image(mask->bitmap(), 0); - drawParallelogram(mask->bitmap(), + drawParallelogram(m_currentData, + mask->bitmap(), m_initialMask->bitmap(), nullptr, corners, bounds.origin()); @@ -730,6 +845,7 @@ void PixelsMovement::drawMask(doc::Mask* mask, bool shrink) } void PixelsMovement::drawParallelogram( + const Transformation& transformation, doc::Image* dst, const doc::Image* src, const doc::Mask* mask, const Transformation::Corners& corners, const gfx::Point& leftTop) @@ -740,7 +856,7 @@ void PixelsMovement::drawParallelogram( // right/straight-angle, we should use the fast rotation algorithm, // as it's pixel-perfect match with the original selection when just // a translation is applied. - double angle = 180.0*m_currentData.angle()/PI; + double angle = 180.0*transformation.angle()/PI; if (std::fabs(std::fmod(std::fabs(angle), 90.0)) < 0.01 || std::fabs(std::fmod(std::fabs(angle), 90.0)-90.0) < 0.01) { rotAlgo = tools::RotationAlgorithm::FAST; @@ -811,14 +927,198 @@ void PixelsMovement::onRotationAlgorithmChange() void PixelsMovement::updateDocumentMask() { - if (!m_setMaskCmd) { - m_setMaskCmd = new cmd::SetMask(m_document, m_currentMask); - m_tx(m_setMaskCmd); - } - else - m_setMaskCmd->setNewMask(m_currentMask); - - m_document->generateMaskBoundaries(m_currentMask); + m_document->setMask(m_currentMask.get()); + m_document->generateMaskBoundaries(m_currentMask.get()); } +void PixelsMovement::flipOriginalImage(const doc::algorithm::FlipType flipType) +{ + // Flip the image. + doc::algorithm::flip_image( + m_originalImage.get(), + gfx::Rect(gfx::Point(0, 0), + gfx::Size(m_originalImage->width(), + m_originalImage->height())), + flipType); + + // Flip the mask. + doc::algorithm::flip_image( + m_initialMask->bitmap(), + gfx::Rect(gfx::Point(0, 0), m_initialMask->bounds().size()), + flipType); +} + +void PixelsMovement::shiftOriginalImage(const int dx, const int dy, + const double angle) +{ + doc::algorithm::shift_image( + m_originalImage.get(), dx, dy, angle); +} + +// Returns the list of cels that will be transformed (the first item +// in the list must be the current cel that was transformed if the cel +// wasn't nullptr). +CelList PixelsMovement::getEditableCels() +{ + CelList cels; + + if (m_site.range().enabled()) { + cels = get_unlocked_unique_cels( + m_site.sprite(), m_site.range()); + } + else { + // TODO This case is used in paste too, where the cel() can be + // nullptr (e.g. we paste the clipboard image into an empty + // cel). + cels.push_back(m_site.cel()); + return cels; + } + + // Current cel (m_site.cel()) can be nullptr when we paste in an + // empty cel (Ctrl+V) and cut (Ctrl+X) the floating pixels. + if (m_site.cel() && + m_site.cel()->layer()->isEditable()) { + auto it = std::find(cels.begin(), cels.end(), m_site.cel()); + if (it != cels.end()) + cels.erase(it); + cels.insert(cels.begin(), m_site.cel()); + } + + return cels; +} + +bool PixelsMovement::gotoFrame(const doc::frame_t deltaFrame) +{ + if (m_site.range().enabled()) { + Layer* layer = m_site.layer(); + ASSERT(layer); + + const doc::SelectedFrames frames = m_site.range().selectedFrames(); + doc::frame_t initialFrame = m_site.frame(); + doc::frame_t frame = initialFrame + deltaFrame; + + if (frames.size() >= 2) { + for (; !frames.contains(frame) && + !layer->cel(frame); frame+=deltaFrame) { + if (deltaFrame > 0 && frame > frames.lastFrame()) { + frame = frames.firstFrame(); + break; + } + else if (deltaFrame < 0 && frame < frames.firstFrame()) { + frame = frames.lastFrame(); + break; + } + } + + if (frame == initialFrame || + !frames.contains(frame) || + // TODO At the moment we don't support going to an empty cel, + // so we don't handle these cases + !layer->cel(frame)) { + return false; + } + + // Rollback all the actions, go to the next/previous frame and + // reproduce all transformation again so the new frame is the + // preview for the transformation. + m_tx.rollbackAndStartAgain(); + + // Re-create the cmd::SetMask() + //m_setMaskCmd = nullptr; + + { + m_canHandleFrameChange = true; + { + ContextWriter writer(m_reader, 1000); + writer.context()->setActiveFrame(frame); + m_site.frame(frame); + } + m_canHandleFrameChange = false; + } + + reproduceAllTransformationsWithInnerCmds(); + return true; + } + } + return false; +} + +// Reproduces all the inner commands in the active m_site +void PixelsMovement::reproduceAllTransformationsWithInnerCmds() +{ + TRACEARGS("MOVPIXS: reproduceAllTransformationsWithInnerCmds", + "layer", m_site.layer()->name(), + "frame", m_site.frame()); + DUMP_INNER_CMDS(); + + m_document->setMask(m_initialMask0.get()); + m_initialMask->copyFrom(m_initialMask0.get()); + m_originalImage.reset( + new_image_from_mask( + m_site, m_initialMask.get(), + Preferences::instance().experimental.newBlend())); + + for (const InnerCmd& c : m_innerCmds) { + switch (c.type) { + case InnerCmd::Clear: + m_tx(new cmd::ClearMask(m_site.cel())); + break; + case InnerCmd::Flip: + flipOriginalImage(c.data.flip.type); + break; + case InnerCmd::Shift: + shiftOriginalImage(c.data.shift.dx, + c.data.shift.dy, + c.data.shift.angle); + break; + case InnerCmd::Stamp: + redrawExtraImage(c.data.stamp.transformation); + stampExtraCelImage(); + break; + } + } + + redrawExtraImage(); + redrawCurrentMask(); + updateDocumentMask(); +} + +#if _DEBUG +void PixelsMovement::dumpInnerCmds() +{ + TRACEARGS("MOVPIXS: InnerCmds size=", m_innerCmds.size()); + for (auto& c : m_innerCmds) { + switch (c.type) { + case InnerCmd::None: + TRACEARGS("MOVPIXS: - None"); + break; + case InnerCmd::Clear: + TRACEARGS("MOVPIXS: - Clear"); + break; + case InnerCmd::Flip: + TRACEARGS("MOVPIXS: - Flip", + (c.data.flip.type == doc::algorithm::FlipHorizontal ? "Horizontal": + "Vertical")); + break; + case InnerCmd::Shift: + TRACEARGS("MOVPIXS: - Shift", + "dx=", c.data.shift.dx, + "dy=", c.data.shift.dy, + "angle=", c.data.shift.angle); + break; + case InnerCmd::Stamp: + TRACEARGS("MOVPIXS: - Stamp", + "angle=", c.data.stamp.transformation->angle(), + "pivot=", c.data.stamp.transformation->pivot().x, + c.data.stamp.transformation->pivot().y, + "bounds=", c.data.stamp.transformation->bounds().x, + c.data.stamp.transformation->bounds().y, + c.data.stamp.transformation->bounds().w, + c.data.stamp.transformation->bounds().h); + break; + } + } +} +#endif // _DEBUG + } // namespace app diff --git a/src/app/ui/editor/pixels_movement.h b/src/app/ui/editor/pixels_movement.h index e9c3c16b9..1eb02bfc1 100644 --- a/src/app/ui/editor/pixels_movement.h +++ b/src/app/ui/editor/pixels_movement.h @@ -12,9 +12,12 @@ #include "app/context_access.h" #include "app/extra_cel.h" #include "app/site.h" +#include "app/transformation.h" #include "app/tx.h" #include "app/ui/editor/handle_type.h" #include "doc/algorithm/flip_type.h" +#include "doc/frame.h" +#include "doc/image_ref.h" #include "gfx/size.h" #include "obs/connection.h" @@ -64,9 +67,9 @@ namespace app { const Image* moveThis, const Mask* mask, const char* operationName); - ~PixelsMovement(); HandleType handle() const { return m_handle; } + bool canHandleFrameChange() const { return m_canHandleFrameChange; } void trim(); void cutMask(); @@ -109,42 +112,89 @@ namespace app { void shift(int dx, int dy); + // Navigate frames + bool gotoFrame(const doc::frame_t deltaFrame); + const Transformation& getTransformation() const { return m_currentData; } private: + void stampImage(bool finalStamp); + void stampExtraCelImage(); void onPivotChange(); void onRotationAlgorithmChange(); - void redrawExtraImage(); + void redrawExtraImage(Transformation* transformation = nullptr); void redrawCurrentMask(); - void drawImage(doc::Image* dst, const gfx::Point& pos, bool renderOriginalLayer); + void drawImage( + const Transformation& transformation, + doc::Image* dst, const gfx::Point& pos, + const bool renderOriginalLayer); void drawMask(doc::Mask* dst, bool shrink); - void drawParallelogram(doc::Image* dst, const doc::Image* src, const doc::Mask* mask, + void drawParallelogram( + const Transformation& transformation, + doc::Image* dst, const doc::Image* src, const doc::Mask* mask, const Transformation::Corners& corners, const gfx::Point& leftTop); void updateDocumentMask(); + void flipOriginalImage(const doc::algorithm::FlipType flipType); + void shiftOriginalImage(const int dx, const int dy, + const double angle); + CelList getEditableCels(); + void reproduceAllTransformationsWithInnerCmds(); +#if _DEBUG + void dumpInnerCmds(); +#endif + const ContextReader m_reader; Site m_site; Doc* m_document; - Sprite* m_sprite; - Layer* m_layer; Tx m_tx; - cmd::SetMask* m_setMaskCmd; bool m_isDragging; bool m_adjustPivot; HandleType m_handle; - Image* m_originalImage; + doc::ImageRef m_originalImage; gfx::Point m_catchPos; Transformation m_initialData; Transformation m_currentData; - Mask* m_initialMask; - Mask* m_currentMask; + std::unique_ptr m_initialMask, m_initialMask0; + std::unique_ptr m_currentMask; bool m_opaque; color_t m_maskColor; obs::scoped_connection m_pivotVisConn; obs::scoped_connection m_pivotPosConn; obs::scoped_connection m_rotAlgoConn; ExtraCelRef m_extraCel; + bool m_canHandleFrameChange; + + // Commands used in the interaction with the transformed pixels. + // This is used to re-create the whole interaction on each + // modified cel when we are modifying multiples cels at the same + // time, or also to re-create it when we switch to another frame. + struct InnerCmd { + enum Type { None, Clear, Flip, Shift, Stamp } type; + union { + struct { + doc::algorithm::FlipType type; + } flip; + struct { + int dx, dy; + double angle; + } shift; + struct { + Transformation* transformation; + } stamp; + } data; + InnerCmd() : type(None) { } + InnerCmd(InnerCmd&&); + ~InnerCmd(); + InnerCmd(const InnerCmd&) = delete; + InnerCmd& operator=(const InnerCmd&) = delete; + static InnerCmd MakeClear(); + static InnerCmd MakeFlip(const doc::algorithm::FlipType flipType); + static InnerCmd MakeShift(const int dx, const int dy, const double angle); + static InnerCmd MakeStamp(const Transformation& t); + }; + std::vector m_innerCmds; }; inline PixelsMovement::MoveModifier& operator|=(PixelsMovement::MoveModifier& a, diff --git a/src/app/ui/timeline/timeline.cpp b/src/app/ui/timeline/timeline.cpp index ed703eb52..db844e6fd 100644 --- a/src/app/ui/timeline/timeline.cpp +++ b/src/app/ui/timeline/timeline.cpp @@ -1793,12 +1793,6 @@ void Timeline::onRemoveFrame(DocEvent& ev) invalidate(); } -void Timeline::onSelectionBoundariesChanged(DocEvent& ev) -{ - if (m_rangeLocks == 0) - clearAndInvalidateRange(); -} - void Timeline::onLayerNameChange(DocEvent& ev) { invalidate(); @@ -1830,7 +1824,7 @@ void Timeline::onAfterFrameChanged(Editor* editor) setFrame(editor->frame(), false); - if (!hasCapture()) + if (!hasCapture() && !editor->keepTimelineRange()) clearAndInvalidateRange(); showCurrentCel(); @@ -4027,7 +4021,12 @@ bool Timeline::onPaste(Context* ctx) bool Timeline::onClear(Context* ctx) { - if (!m_document || !m_sprite || !m_range.enabled()) + if (!m_document || + !m_sprite || + !m_range.enabled() || + // If the mask is visible the delete command will be handled by + // the Editor + m_document->isMaskVisible()) return false; Command* cmd = nullptr; diff --git a/src/app/ui/timeline/timeline.h b/src/app/ui/timeline/timeline.h index 4ec2338d7..bf7e16f7a 100644 --- a/src/app/ui/timeline/timeline.h +++ b/src/app/ui/timeline/timeline.h @@ -130,6 +130,8 @@ namespace app { void lockRange(); void unlockRange(); + void clearAndInvalidateRange(); + protected: bool onProcessMessage(ui::Message* msg) override; void onInitTheme(ui::InitThemeEvent& ev) override; @@ -145,7 +147,6 @@ namespace app { void onAfterRemoveLayer(DocEvent& ev) override; void onAddFrame(DocEvent& ev) override; void onRemoveFrame(DocEvent& ev) override; - void onSelectionBoundariesChanged(DocEvent& ev) override; void onLayerNameChange(DocEvent& ev) override; void onAddFrameTag(DocEvent& ev) override; void onRemoveFrameTag(DocEvent& ev) override; @@ -312,7 +313,6 @@ namespace app { const Cel* cel); void updateDropRange(const gfx::Point& pt); void clearClipboardRange(); - void clearAndInvalidateRange(); // The layer of the bottom (e.g. Background layer) layer_t firstLayer() const { return 0; } diff --git a/src/app/util/clipboard.cpp b/src/app/util/clipboard.cpp index 3e1bf6447..c3ea99fa0 100644 --- a/src/app/util/clipboard.cpp +++ b/src/app/util/clipboard.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2019 Igara Studio S.A. +// Copyright (C) 2019 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -33,6 +33,7 @@ #include "app/util/clipboard.h" #include "app/util/clipboard_native.h" #include "app/util/new_image_from_mask.h" +#include "app/util/range_utils.h" #include "clip/clip.h" #include "doc/doc.h" #include "render/dithering.h" @@ -239,6 +240,28 @@ void get_document_range_info(Doc** document, DocRange* range) } } +void clear_mask_from_cels(Tx& tx, + Doc* doc, + const CelList& cels, + const bool deselectMask) +{ + for (Cel* cel : cels) { + ObjectId celId = cel->id(); + + tx(new cmd::ClearMask(cel)); + + // Get cel again just in case the cmd::ClearMask() called cmd::ClearCel() + cel = doc::get(celId); + if (cel && + cel->layer()->isTransparent()) { + tx(new cmd::TrimCel(cel)); + } + } + + if (deselectMask) + tx(new cmd::DeselectMask(doc)); +} + void clear_content() { set_clipboard_image(nullptr, nullptr, nullptr, true, false); @@ -257,14 +280,18 @@ void cut(ContextWriter& writer) else { { Tx tx(writer.context(), "Cut"); - tx(new cmd::ClearMask(writer.cel())); - - ASSERT(writer.cel()); - if (writer.cel() && - writer.cel()->layer()->isTransparent()) - tx(new cmd::TrimCel(writer.cel())); - - tx(new cmd::DeselectMask(writer.document())); + Site site = writer.context()->activeSite(); + CelList cels; + if (site.range().enabled()) { + cels = get_unlocked_unique_cels(site.sprite(), site.range()); + } + else if (site.cel()) { + cels.push_back(site.cel()); + } + clear_mask_from_cels(tx, + writer.document(), + cels, + true); // Deselect mask tx.commit(); } writer.document()->generateMaskBoundaries(); @@ -369,6 +396,10 @@ void paste(Context* ctx, const bool interactive) } if (current_editor && interactive) { + // TODO we don't support pasting in multiple cels at the moment, + // so we clear the range here. + App::instance()->timeline()->clearAndInvalidateRange(); + // Change to MovingPixelsState current_editor->pasteImage(src_image.get(), clipboard_mask.get()); diff --git a/src/app/util/clipboard.h b/src/app/util/clipboard.h index 19f63242f..2959d1eea 100644 --- a/src/app/util/clipboard.h +++ b/src/app/util/clipboard.h @@ -9,6 +9,7 @@ #define APP_UTIL_CLIPBOARD_H_INCLUDED #pragma once +#include "doc/cel_list.h" #include "gfx/point.h" #include "gfx/size.h" #include "ui/base.h" @@ -22,11 +23,12 @@ namespace doc { } namespace app { - class Doc; class Context; class ContextReader; class ContextWriter; + class Doc; class DocRange; + class Tx; namespace clipboard { using namespace doc; @@ -56,6 +58,11 @@ namespace app { ClipboardFormat get_current_format(); void get_document_range_info(Doc** document, DocRange* range); + void clear_mask_from_cels(Tx& tx, + Doc* doc, + const doc::CelList& cels, + const bool deselectMask); + void clear_content(); void cut(ContextWriter& context); void copy(const ContextReader& context);