Fix multi layer movement in tilemap mode (fix #3144)

Before this fix, a multi-layer mask move (with mixed layer types:
normal layer and tilemap layers with different grids) caused loss of
drawing areas.

The heart of this solution is to correctly align the 'selection mask'
and 'transform data' according to the layer's grid, and also, forcing
'site' TilemapMode/TilesetMode before each
reproduceAllTransformationsWithInnerCmds() iteration.

The scale transformation could have some things to improve in later
commits. Not fully tested yet.
This commit is contained in:
Gaspar Capello 2022-03-04 17:18:40 -03:00 committed by David Capello
parent 70a42c4c77
commit bc050e8f15
4 changed files with 223 additions and 24 deletions

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2021 Igara Studio S.A.
// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -369,14 +369,32 @@ void PixelsMovement::moveImage(const gfx::PointF& pos, MoveModifier moveModifier
gfx::RectF bounds = m_initialData.bounds();
gfx::PointF abs_initial_pivot = m_initialData.pivot();
gfx::PointF abs_pivot = m_currentData.pivot();
const bool tilemapModeOrSnapToGrid =
(m_site.tilemapMode() == TilemapMode::Tiles && m_site.layer()->isTilemap()) ||
(moveModifier & SnapToGridMovement) == SnapToGridMovement;
auto newTransformation = m_currentData;
gfx::Point initialDataOrigin;
switch (m_handle) {
case MovePixelsHandle: {
double dx = (pos.x - m_catchPos.x);
double dy = (pos.y - m_catchPos.y);
if (tilemapModeOrSnapToGrid) {
initialDataOrigin = bounds.origin();
if (m_catchPos.x == 0 && m_catchPos.y == 0) {
// Movement through keyboard:
dx *= m_site.gridBounds().w;
dy *= m_site.gridBounds().h;
}
else {
// Movement through mouse/trackpad:
dx = double(int(dx) / m_site.gridBounds().w * m_site.gridBounds().w);
dy = double(int(dy) / m_site.gridBounds().h * m_site.gridBounds().h);
}
}
if ((moveModifier & FineControl) == 0) {
if (dx >= 0.0) { dx = std::floor(dx); } else { dx = std::ceil(dx); }
if (dy >= 0.0) { dy = std::floor(dy); } else { dy = std::ceil(dy); }
@ -390,26 +408,10 @@ void PixelsMovement::moveImage(const gfx::PointF& pos, MoveModifier moveModifier
}
bounds.offset(dx, dy);
if ((m_site.tilemapMode() == TilemapMode::Tiles) ||
(moveModifier & SnapToGridMovement) == SnapToGridMovement) {
// Snap the x1,y1 point to the grid.
gfx::Rect gridBounds = m_site.gridBounds();
gfx::PointF gridOffset(
snap_to_grid(
gridBounds,
gfx::Point(bounds.origin()),
PreferSnapTo::ClosestGridVertex));
// Now we calculate the difference from x1,y1 point and we can
// use it to adjust all coordinates (x1, y1, x2, y2).
bounds.setOrigin(gridOffset);
}
newTransformation.bounds(bounds);
newTransformation.pivot(abs_initial_pivot +
bounds.origin() -
m_initialData.bounds().origin());
initialDataOrigin);
break;
}
@ -712,6 +714,7 @@ void PixelsMovement::moveImage(const gfx::PointF& pos, MoveModifier moveModifier
void PixelsMovement::getDraggedImageCopy(std::unique_ptr<Image>& outputImage,
std::unique_ptr<Mask>& outputMask)
{
ASSERT(!(m_site.tilemapMode() == TilemapMode::Tiles && !m_site.layer()->isTilemap()));
gfx::Rect bounds = m_currentData.transformedBounds();
if (bounds.isEmpty())
return;
@ -759,6 +762,29 @@ void PixelsMovement::getDraggedImageCopy(std::unique_ptr<Image>& outputImage,
outputMask.reset(mask.release());
}
void PixelsMovement::alignMasksAndTransformData(
const Mask* initialMask0,
const Mask* initialMask,
const Mask* currentMask,
const Transformation* initialData,
const Transformation* currentData,
const doc::Grid& grid,
const gfx::SizeF& initialScaleRatio)
{
m_initialMask0->replace(Mask(grid.makeAlignedMask(initialMask0)));
m_initialMask->replace(Mask(grid.makeAlignedMask(initialMask)));
m_currentMask->replace(Mask(grid.makeAlignedMask(currentMask)));
gfx::RectF iniBounds = grid.alignBounds(initialData->bounds());
m_initialData.bounds(iniBounds);
m_currentData.bounds(gfx::RectF(gfx::PointF(grid.alignBounds(currentData->bounds()).origin()),
gfx::SizeF(initialScaleRatio.w == 1.0 ?
iniBounds.w :
m_currentData.bounds().w,
initialScaleRatio.h == 1.0 ?
iniBounds.h :
m_currentData.bounds().h)));
}
void PixelsMovement::stampImage()
{
stampImage(false);
@ -791,14 +817,101 @@ void PixelsMovement::stampImage(bool finalStamp)
stampExtraCelImage();
}
// Saving original values before the 'for' loop
TilemapMode originalSiteTilemapMode = (m_site.tilemapMode() == TilemapMode::Tiles &&
m_site.layer()->isTilemap()? TilemapMode::Tiles : TilemapMode::Pixels);
TilesetMode originalSiteTilesetMode = m_site.tilesetMode();
std::unique_ptr<Mask> initialMask0(new Mask(*m_initialMask0.get()));
std::unique_ptr<Mask> initialMask(new Mask(*m_initialMask.get()));
std::unique_ptr<Mask> currentMask(new Mask(*m_currentMask.get()));
std::unique_ptr<Transformation> initialData(new Transformation(m_initialData));
std::unique_ptr<Transformation> currentData(new Transformation(m_currentData));
gfx::SizeF initialScaleRatio(double(m_currentData.bounds().w) / double(m_initialData.bounds().w),
double(m_currentData.bounds().h) / double(m_initialData.bounds().h));
bool lastProcessedLayerWasTilemap;
Grid lastGrid = m_site.grid();
if (originalSiteTilemapMode == TilemapMode::Tiles) {
alignMasksAndTransformData(initialMask0.get(),
initialMask.get(),
currentMask.get(),
initialData.get(),
currentData.get(),
lastGrid,
initialScaleRatio);
lastProcessedLayerWasTilemap = true;
}
else
lastProcessedLayerWasTilemap = false;
Grid targetGrid(m_site.grid());
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());
targetGrid = m_site.grid();
// Preparing masks and transform Data before to reproduceAllTransformationsWithInnerCmds
if (lastProcessedLayerWasTilemap && target->layer()->isTilemap()) {
// Need to re-align masks and transform data of a new tilemap.
// The 'temp' buffers has to be re-calculated if the last grid is different
// compared with the last one 'lastGrid'.
if (originalSiteTilemapMode == TilemapMode::Tiles && targetGrid != lastGrid) {
alignMasksAndTransformData(initialMask0.get(),
initialMask.get(),
currentMask.get(),
initialData.get(),
currentData.get(),
targetGrid,
initialScaleRatio);
lastGrid = targetGrid;
lastProcessedLayerWasTilemap = true;
}
else
lastProcessedLayerWasTilemap = false;
}
else if (lastProcessedLayerWasTilemap && !target->layer()->isTilemap()) {
// Convert masks and transform data to initial
m_initialMask0->replace(*initialMask0.get());
m_initialMask->replace(*initialMask.get());
m_currentMask->replace(*currentMask.get());
m_initialData.bounds(initialData.get()->bounds());
m_currentData.bounds(currentData.get()->bounds());
lastProcessedLayerWasTilemap = false;
}
else if (!lastProcessedLayerWasTilemap && target->layer()->isTilemap()) {
// Align masks and transforms data to initial
if (originalSiteTilemapMode == TilemapMode::Tiles) {
alignMasksAndTransformData(initialMask0.get(),
initialMask.get(),
currentMask.get(),
initialData.get(),
currentData.get(),
targetGrid,
initialScaleRatio);
lastGrid = targetGrid;
lastProcessedLayerWasTilemap = true;
}
else {
// Do nothing, because 'lastProcessedLayerWasTilemap' was NO tilemap,
// so 'm_initialData' and 'm_currentData' is correctly adjusted from
// the previous 'for' iteration.
lastProcessedLayerWasTilemap = false;
}
}
else {// !lastProcessedLayerWasTilemap && !target->layer()->isTilemap()
// Do nothing
lastProcessedLayerWasTilemap = false;
}
ASSERT(m_site.cel() == target);
if (originalSiteTilemapMode == TilemapMode::Tiles && target->layer()->isTilemap())
m_site.tilemapMode(TilemapMode::Tiles);
else
m_site.tilemapMode(TilemapMode::Pixels);
if (originalSiteTilemapMode == TilemapMode::Pixels)
m_site.tilesetMode(TilesetMode::Auto);
reproduceAllTransformationsWithInnerCmds();
}
@ -806,12 +919,20 @@ void PixelsMovement::stampImage(bool finalStamp)
stampExtraCelImage();
}
m_initialMask0->replace(*initialMask0.get());
m_initialMask->replace(*initialMask.get());
m_currentMask->replace(*currentMask.get());
m_initialData.bounds(initialData.get()->bounds());
m_currentData.bounds(currentData.get()->bounds());
m_site.tilesetMode(originalSiteTilesetMode);
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());
m_site.tilemapMode(originalSiteTilemapMode);
m_site.tilesetMode(originalSiteTilesetMode);
redrawExtraImage();
}
}
@ -988,6 +1109,7 @@ void PixelsMovement::redrawExtraImage(Transformation* transformation)
if (!m_extraCel)
m_extraCel.reset(new ExtraCel);
ASSERT(!(m_site.tilemapMode() == TilemapMode::Tiles && !m_site.layer()->isTilemap()));
gfx::Rect bounds = transformation->transformedBounds();
if (!bounds.isEmpty()) {
@ -995,6 +1117,7 @@ void PixelsMovement::redrawExtraImage(Transformation* transformation)
if (m_site.tilemapMode() == TilemapMode::Tiles) {
// Transforming tiles
extraCelSize = m_site.grid().canvasToTile(bounds).size();
bounds = m_site.grid().alignBounds(bounds);
}
else {
// Transforming pixels
@ -1041,7 +1164,7 @@ void PixelsMovement::drawImage(
auto corners = transformation.transformedCorners();
gfx::Rect bounds = corners.bounds(transformation.cornerThick());
if (m_site.tilemapMode() == TilemapMode::Tiles) {
if (m_site.tilemapMode() == TilemapMode::Tiles && m_site.layer()->isTilemap()) {
dst->setMaskColor(doc::notile);
dst->clear(dst->maskColor());
@ -1402,10 +1525,17 @@ void PixelsMovement::reproduceAllTransformationsWithInnerCmds()
m_document->setMask(m_initialMask0.get());
m_initialMask->copyFrom(m_initialMask0.get());
m_originalImage.reset(
if (m_site.layer()->isTilemap() && m_site.tilemapMode() == TilemapMode::Tiles) {
m_originalImage.reset(
new_tilemap_from_mask(
m_site, m_initialMask0.get()));
}
else {
m_originalImage.reset(
new_image_from_mask(
m_site, m_initialMask.get(),
m_site, m_initialMask0.get(),
Preferences::instance().experimental.newBlend()));
}
for (const InnerCmd& c : m_innerCmds) {
switch (c.type) {

View File

@ -159,6 +159,15 @@ namespace app {
const double angle);
CelList getEditableCels();
void reproduceAllTransformationsWithInnerCmds();
void alignMasksAndTransformData(const Mask* initialMask0,
const Mask* initialMask,
const Mask* currentMask,
const Transformation* initialData,
const Transformation* currentData,
const doc::Grid& grid,
const gfx::SizeF& initialScaleRatio);
#if _DEBUG
void dumpInnerCmds();
#endif

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (c) 2019-2021 Igara Studio S.A.
// Copyright (c) 2019-2022 Igara Studio S.A.
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
@ -13,6 +13,7 @@
#include "doc/image.h"
#include "doc/image_impl.h"
#include "doc/image_ref.h"
#include "doc/mask.h"
#include "doc/primitives.h"
#include "gfx/point.h"
#include "gfx/rect.h"
@ -183,4 +184,49 @@ std::vector<gfx::Point> Grid::tilesInCanvasRegion(const gfx::Region& rgn) const
return result;
}
Mask Grid::makeAlignedMask(const Mask* mask) const
{
// Fact: the newBounds will be always larger or equal than oldBounds
Mask maskOutput;
if (mask->isFrozen()) {
ASSERT(false);
return maskOutput;
}
gfx::Rect oldBounds = mask->bounds();
gfx::Rect newBounds = alignBounds(mask->bounds());
ASSERT(newBounds.w > 0 && newBounds.h > 0);
ImageRef newBitmap;
if (!mask->bitmap()) {
maskOutput.replace(newBounds);
return maskOutput;
}
newBitmap.reset(Image::create(IMAGE_BITMAP, newBounds.w, newBounds.h));
maskOutput.freeze();
maskOutput.reserve(newBounds);
const LockImageBits<BitmapTraits> bits(mask->bitmap());
typename LockImageBits<BitmapTraits>::const_iterator it = bits.begin();
// We must travel thought the old bitmap and masking the new bitmap
gfx::Point previousPoint(std::numeric_limits<int>::max(), std::numeric_limits<int>::max());
for (int y=0; y < oldBounds.h; ++y) {
for (int x=0; x < oldBounds.w; ++x, ++it) {
ASSERT(it != bits.end());
if (*it) {
gfx::Rect newBoundsTile = alignBounds(gfx::Rect(oldBounds.x + x, oldBounds.y + y, 1, 1));
if (previousPoint != newBoundsTile.origin()) {
// Fill a tile region in the newBitmap
fill_rect(maskOutput.bitmap(),
gfx::Rect(newBoundsTile.x - newBounds.x, newBoundsTile.y - newBounds.y,
tileSize().w, tileSize().h),
1);
previousPoint = newBoundsTile.origin();
}
}
}
}
maskOutput.unfreeze();
return maskOutput;
}
} // namespace doc

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (c) 2019-2020 Igara Studio S.A.
// Copyright (c) 2019-2022 Igara Studio S.A.
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
@ -15,6 +15,8 @@
namespace doc {
class Mask;
class Grid {
public:
Grid(const gfx::Size& sz = gfx::Size(16, 16))
@ -65,6 +67,18 @@ namespace doc {
// Returns an array of tile positions that are touching the given region in the canvas
std::vector<gfx::Point> tilesInCanvasRegion(const gfx::Region& rgn) const;
// Returns a mask aligned to the current grid, starting from other not aligned mask
Mask makeAlignedMask(const Mask* mask) const;
inline bool operator!=(const Grid& gridB) const {
return (this->tileSize() != gridB.tileSize() ||
this->origin() != gridB.origin() ||
this->tileOffset() != gridB.tileOffset() ||
this->oddColOffset() != gridB.oddColOffset() ||
this->oddRowOffset() != gridB.oddRowOffset() ||
this->tileCenter() != gridB.tileCenter());// Perhaps this last condition isn't needed.
}
private:
gfx::Size m_tileSize;
gfx::Point m_origin;