mirror of
https://github.com/aseprite/aseprite.git
synced 2025-02-11 09:40:42 +00:00
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:
parent
70a42c4c77
commit
bc050e8f15
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user