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
This commit is contained in:
David Capello 2019-09-05 15:03:13 -03:00
parent 2d979c521c
commit 3e478d3efa
18 changed files with 659 additions and 181 deletions

View File

@ -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

View File

@ -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();

View File

@ -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());
}

View File

@ -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());
}

View File

@ -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)

View File

@ -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;

View File

@ -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);
}

View File

@ -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();
}

View File

@ -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<MovingPixelsState*>(m_state.get())) {
if (movingPixels->canHandleFrameChange())
return true;
}
return false;
}
EditorHit Editor::calcHit(const gfx::Point& mouseScreenPos)
{
tools::Ink* ink = getCurrentEditorInk();

View File

@ -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);

View File

@ -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)

View File

@ -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);

View File

@ -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 <algorithm>
#if _DEBUG
#define DUMP_INNER_CMDS() dumpInnerCmds()
#else
#define DUMP_INNER_CMDS()
#endif
namespace app {
template<typename T>
@ -50,6 +60,60 @@ static inline const base::Vector2d<double> point2Vector(const gfx::PointT<T>& pt
return base::Vector2d<double>(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<Image>& outputImage,
std::unique_ptr<Mask>& outputMask)
{
gfx::Rect bounds = m_currentData.transformedBounds();
std::unique_ptr<Image> image(Image::create(m_sprite->pixelFormat(), bounds.w, bounds.h));
std::unique_ptr<Image> 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<Image>& 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<LayerImage*>(m_layer)->opacity(): 255);
if (!transformation)
transformation = &m_currentData;
int t, opacity = (m_site.layer()->isImage() ?
static_cast<LayerImage*>(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<LayerImage*>(m_layer)->blendMode():
m_extraCel->setBlendMode(m_site.layer()->isImage() ?
static_cast<LayerImage*>(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

View File

@ -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<Mask> m_initialMask, m_initialMask0;
std::unique_ptr<Mask> 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<InnerCmd> m_innerCmds;
};
inline PixelsMovement::MoveModifier& operator|=(PixelsMovement::MoveModifier& a,

View File

@ -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;

View File

@ -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; }

View File

@ -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<Cel>(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());

View File

@ -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);