Add support to clear/fill/stroke selections in tiles mode

This commit is contained in:
David Capello 2020-08-27 20:32:22 -03:00
parent 4eeaad5a69
commit b1c0d80356
21 changed files with 289 additions and 85 deletions

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2020 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -16,6 +17,7 @@
#include "doc/cel.h"
#include "doc/image_impl.h"
#include "doc/layer.h"
#include "doc/layer_tilemap.h"
#include "doc/mask.h"
#include "doc/primitives.h"
@ -33,6 +35,9 @@ ClearMask::ClearMask(Cel* cel)
// entire image in the cel.
if (!doc->isMaskVisible()) {
m_seq.add(new cmd::ClearCel(cel));
// In this case m_copy will be nullptr, so the clear()/restore()
// member functions will have no effect.
return;
}
@ -41,60 +46,78 @@ ClearMask::ClearMask(Cel* cel)
if (!image)
return;
Mask* mask = doc->mask();
m_offset = mask->bounds().origin() - cel->position();
const Mask* mask = doc->mask();
gfx::Rect imageBounds;
gfx::Rect maskBounds;
if (image->pixelFormat() == IMAGE_TILEMAP) {
auto grid = cel->grid();
imageBounds = gfx::Rect(grid.canvasToTile(cel->position()),
cel->image()->size());
maskBounds = grid.canvasToTile(mask->bounds());
m_bgcolor = doc::tile_i_notile; // TODO configurable empty tile
}
else {
imageBounds = cel->bounds();
maskBounds = mask->bounds();
m_bgcolor = doc->bgColor(cel->layer());
}
gfx::Rect bounds =
image->bounds().createIntersection(
gfx::Rect(
m_offset.x, m_offset.y,
mask->bounds().w, mask->bounds().h));
if (bounds.isEmpty())
gfx::Rect cropBounds = (imageBounds & maskBounds);
if (cropBounds.isEmpty())
return;
m_dstImage.reset(new WithImage(image));
m_bgcolor = doc->bgColor(cel->layer());
m_boundsX = bounds.x;
m_boundsY = bounds.y;
cropBounds.offset(-imageBounds.origin());
m_cropPos = cropBounds.origin();
m_copy.reset(crop_image(image,
bounds.x, bounds.y, bounds.w, bounds.h, m_bgcolor));
m_copy.reset(crop_image(image, cropBounds, m_bgcolor));
}
void ClearMask::onExecute()
{
m_seq.execute(context());
if (m_dstImage)
clear();
clear();
}
void ClearMask::onUndo()
{
if (m_dstImage)
restore();
restore();
m_seq.undo();
}
void ClearMask::onRedo()
{
m_seq.redo();
if (m_dstImage)
clear();
clear();
}
void ClearMask::clear()
{
if (!m_copy)
return;
Cel* cel = this->cel();
Image* image = m_dstImage->image();
Doc* doc = static_cast<Doc*>(cel->document());
Mask* mask = doc->mask();
doc::algorithm::fill_selection(image, m_offset, mask, m_bgcolor);
Grid grid = cel->grid();
doc::algorithm::fill_selection(
cel->image(),
cel->bounds(),
mask,
m_bgcolor,
(cel->image()->isTilemap() ? &grid: nullptr));
}
void ClearMask::restore()
{
copy_image(m_dstImage->image(), m_copy.get(), m_boundsX, m_boundsY);
if (!m_copy)
return;
Cel* cel = this->cel();
copy_image(cel->image(),
m_copy.get(),
m_cropPos.x,
m_cropPos.y);
}
} // namespace cmd

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2020 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -13,6 +14,7 @@
#include "app/cmd/with_image.h"
#include "app/cmd_sequence.h"
#include "doc/image_ref.h"
#include "gfx/rect.h"
#include <memory>
@ -39,10 +41,8 @@ namespace cmd {
void restore();
CmdSequence m_seq;
std::unique_ptr<WithImage> m_dstImage;
ImageRef m_copy;
gfx::Point m_offset;
int m_boundsX, m_boundsY;
gfx::Point m_cropPos;
color_t m_bgcolor;
};

View File

@ -74,7 +74,11 @@ void FillCommand::onExecute(Context* ctx)
return;
Preferences& pref = Preferences::instance();
app::Color color = pref.colorBar.fgColor();
doc::color_t color;
if (site.tilemapMode() == TilemapMode::Tiles)
color = pref.colorBar.fgTile();
else
color = color_utils::color_for_layer(pref.colorBar.fgColor(), layer);
{
Tx tx(writer.context(), "Fill Selection with Foreground Color");
@ -88,25 +92,28 @@ void FillCommand::onExecute(Context* ctx)
mask->bounds());
expand.validateDestCanvas(rgn);
const gfx::Point offset = (mask->bounds().origin()
- expand.getCel()->position());
const doc::color_t docColor =
color_utils::color_for_layer(
color, layer);
gfx::Rect imageBounds(expand.getCel()->position(),
expand.getDestCanvas()->size());
doc::Grid grid = site.grid();
if (site.tilemapMode() == TilemapMode::Tiles)
imageBounds = grid.tileToCanvas(imageBounds);
if (m_type == Stroke) {
doc::algorithm::stroke_selection(
expand.getDestCanvas(),
offset,
imageBounds,
mask,
docColor);
color,
(site.tilemapMode() == TilemapMode::Tiles ? &grid: nullptr));
}
else {
doc::algorithm::fill_selection(
expand.getDestCanvas(),
offset,
imageBounds,
mask,
docColor);
color,
(site.tilemapMode() == TilemapMode::Tiles ? &grid: nullptr));
}
expand.commit();

View File

@ -1062,25 +1062,18 @@ void GradientInkProcessing<IndexedTraits>::processPixel(int x, int y)
template<typename ImageTraits>
class XorInkProcessing : public DoubleInkProcessing<XorInkProcessing<ImageTraits>, ImageTraits> {
public:
XorInkProcessing(ToolLoop* loop) {
m_color = loop->getPrimaryColor();
}
void processPixel(int x, int y) {
// Do nothing
}
private:
color_t m_color;
XorInkProcessing(ToolLoop* loop) { }
void processPixel(int x, int y) { }
};
template<>
void XorInkProcessing<RgbTraits>::processPixel(int x, int y) {
*m_dstAddress = rgba_blender_neg_bw(*m_srcAddress, m_color, 255);
*m_dstAddress = rgba_blender_neg_bw(*m_srcAddress, 0, 255);
}
template<>
void XorInkProcessing<GrayscaleTraits>::processPixel(int x, int y) {
*m_dstAddress = graya_blender_neg_bw(*m_srcAddress, m_color, 255);
*m_dstAddress = graya_blender_neg_bw(*m_srcAddress, 0, 255);
}
template<>
@ -1088,19 +1081,17 @@ class XorInkProcessing<IndexedTraits> : public DoubleInkProcessing<XorInkProcess
public:
XorInkProcessing(ToolLoop* loop) :
m_palette(get_current_palette()),
m_rgbmap(loop->getRgbMap()),
m_color(m_palette->getEntry(loop->getPrimaryColor())) {
m_rgbmap(loop->getRgbMap()) {
}
void processPixel(int x, int y) {
color_t c = rgba_blender_neg_bw(m_palette->getEntry(*m_srcAddress), m_color, 255);
color_t c = rgba_blender_neg_bw(m_palette->getEntry(*m_srcAddress), 0, 255);
*m_dstAddress = m_rgbmap->mapColor(c);
}
private:
const Palette* m_palette;
const RgbMap* m_rgbmap;
color_t m_color;
};
//////////////////////////////////////////////////////////////////////

View File

@ -280,6 +280,7 @@ void PixelsMovement::cutMask()
if (writer.cel()) {
clear_mask_from_cel(m_tx,
writer.cel(),
m_site.tilemapMode(),
m_site.tilesetMode());
// Do not trim here so we don't lost the information about all
@ -1100,6 +1101,7 @@ void PixelsMovement::reproduceAllTransformationsWithInnerCmds()
case InnerCmd::Clear:
clear_mask_from_cel(m_tx,
m_site.cel(),
m_site.tilemapMode(),
m_site.tilesetMode());
break;
case InnerCmd::Flip:

View File

@ -612,13 +612,14 @@ void modify_tilemap_cel_region(
void clear_mask_from_cel(CmdSequence* cmds,
doc::Cel* cel,
const TilemapMode tilemapMode,
const TilesetMode tilesetMode)
{
ASSERT(cmds);
ASSERT(cel);
ASSERT(cel->layer());
if (cel->layer()->isTilemap()) {
if (cel->layer()->isTilemap() && tilemapMode == TilemapMode::Pixels) {
Doc* doc = static_cast<Doc*>(cel->document());
// Simple case (there is no visible selection, so we remove the
@ -640,9 +641,10 @@ void clear_mask_from_cel(CmdSequence* cmds,
doc::ImageRef modified(doc::Image::createCopy(origTile.get()));
doc::algorithm::fill_selection(
modified.get(),
mask->bounds().origin() - tileBoundsInCanvas.origin(),
tileBoundsInCanvas,
mask,
bgcolor);
bgcolor,
nullptr);
return modified;
});
}

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2019-2020 Igara Studio S.A.
// Copyright (C) 2001-2016 David Capello
//
// This program is distributed under the terms of
@ -9,6 +9,7 @@
#define APP_UTIL_CEL_OPS_H_INCLUDED
#pragma once
#include "app/tilemap_mode.h"
#include "app/tileset_mode.h"
#include "doc/color.h"
#include "doc/frame.h"
@ -69,6 +70,7 @@ namespace app {
void clear_mask_from_cel(
CmdSequence* cmds,
doc::Cel* cel,
const TilemapMode tilemapMode,
const TilesetMode tilesetMode);
// unusedTiles is a set of possibles tiles to check if they are

View File

@ -253,7 +253,10 @@ void clear_mask_from_cels(Tx& tx,
ObjectId celId = cel->id();
clear_mask_from_cel(
tx, cel, ColorBar::instance()->tilesetMode());
tx, cel,
// TODO use Site information instead of color bar
ColorBar::instance()->tilemapMode(),
ColorBar::instance()->tilesetMode());
// Get cel again just in case the cmd::ClearMask() called cmd::ClearCel()
cel = doc::get<Cel>(celId);

View File

@ -1,4 +1,5 @@
// Aseprite Document Library
// Copyright (c) 2020 Igara Studio S.A.
// Copyright (c) 2018 David Capello
//
// This file is released under the terms of the MIT license.
@ -10,6 +11,7 @@
#include "doc/algorithm/fill_selection.h"
#include "doc/grid.h"
#include "doc/image_impl.h"
#include "doc/mask.h"
#include "doc/primitives.h"
@ -18,27 +20,40 @@ namespace doc {
namespace algorithm {
void fill_selection(Image* image,
const gfx::Point& offset,
const gfx::Rect& imageBounds,
const Mask* mask,
const color_t color)
const color_t color,
const Grid* grid)
{
ASSERT(mask);
ASSERT(mask->bitmap());
if (!mask || !mask->bitmap())
return;
const LockImageBits<BitmapTraits> maskBits(mask->bitmap());
LockImageBits<BitmapTraits>::const_iterator it = maskBits.begin();
const auto rc = (imageBounds & mask->bounds());
if (rc.isEmpty())
return; // <- There is no intersection between image bounds and mask bounds
const gfx::Rect maskBounds = mask->bounds();
for (int v=0; v<maskBounds.h; ++v) {
for (int u=0; u<maskBounds.w; ++u, ++it) {
const LockImageBits<BitmapTraits> maskBits(mask->bitmap(),
gfx::Rect(rc).offset(-mask->origin()));
auto it = maskBits.begin();
for (int v=0; v<rc.h; ++v) {
for (int u=0; u<rc.w; ++u, ++it) {
ASSERT(it != maskBits.end());
if (*it) {
// TODO use iterators
put_pixel(image,
u + offset.x,
v + offset.y, color);
gfx::Point pt(u + rc.x,
v + rc.y);
if (grid) {
pt = grid->canvasToTile(pt);
}
else {
pt -= imageBounds.origin();
}
// TODO use iterator
put_pixel(image, pt.x, pt.y, color);
}
}
}

View File

@ -1,4 +1,5 @@
// Aseprite Document Library
// Copyright (c) 2020 Igara Studio S.A.
// Copyright (c) 2018 David Capello
//
// This file is released under the terms of the MIT license.
@ -9,17 +10,23 @@
#pragma once
#include "doc/color.h"
#include "gfx/point.h"
#include "gfx/rect.h"
namespace doc {
class Grid;
class Image;
class Mask;
namespace algorithm {
void fill_selection(Image* image,
const gfx::Point& offset,
const Mask* mask,
const color_t color);
void fill_selection(
Image* image,
const gfx::Rect& imageBounds,
const Mask* mask,
// This can be a color_t or a tile_t if the image is a tilemap
const color_t color,
// Optional grid for tilemaps
const Grid* grid = nullptr);
} // namespace algorithm
} // namespace doc

View File

@ -0,0 +1,101 @@
// Aseprite Document Library
// Copyright (c) 2020 Igara Studio S.A.
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
#include "gtest/gtest.h"
#include "doc/algorithm/fill_selection.h"
#include "doc/grid.h"
#include "doc/image.h"
#include "doc/mask.h"
using namespace doc;
using namespace gfx;
::testing::AssertionResult cmp_img(const std::vector<color_t>& pixels,
const Image* image)
{
int c = 0;
for (int y=0; y<image->height(); ++y) {
for (int x=0; x<image->width(); ++x) {
if (pixels[c] != image->getPixel(x, y)) {
return ::testing::AssertionFailure()
<< "ExpectedPixel=" << (int)pixels[c]
<< " ActualPixel=" << (int)image->getPixel(x, y)
<< " x=" << x
<< " y=" << y;
}
++c;
}
}
return ::testing::AssertionSuccess();
}
TEST(FillSelection, Image)
{
ImageRef image(Image::create(IMAGE_INDEXED, 4, 4));
image->clear(1);
// No-op (no intersection between image & mask)
Mask mask;
mask.replace(Rect(0, 0, 1, 5));
algorithm::fill_selection(image.get(), Rect(1, 1, 4, 4), &mask, 2, nullptr);
EXPECT_TRUE(cmp_img({ 1, 1, 1, 1,
1, 1, 1, 1,
1, 1, 1, 1,
1, 1, 1, 1 }, image.get()));
mask.replace(Rect(1, 0, 2, 3));
algorithm::fill_selection(image.get(), Rect(1, 1, 4, 4), &mask, 2, nullptr);
EXPECT_TRUE(cmp_img({ 2, 2, 1, 1,
2, 2, 1, 1,
1, 1, 1, 1,
1, 1, 1, 1 }, image.get()));
mask.replace(Rect(2, 2, 2, 3));
algorithm::fill_selection(image.get(), Rect(1, 3, 4, 4), &mask, 3, nullptr);
EXPECT_TRUE(cmp_img({ 2, 3, 3, 1,
2, 3, 3, 1,
1, 1, 1, 1,
1, 1, 1, 1 }, image.get()));
}
TEST(FillSelection, Tilemap)
{
ImageRef image(Image::create(IMAGE_TILEMAP, 4, 4));
image->clear(1);
Grid grid(Size(8, 8));
grid.origin(Point(4, 4));
// No-op (no intersection between image & mask)
Mask mask;
mask.replace(Rect(0, 0, 4, 4));
algorithm::fill_selection(image.get(), Rect(4, 4, 32, 32), &mask, 2, &grid);
EXPECT_TRUE(cmp_img({ 1, 1, 1, 1,
1, 1, 1, 1,
1, 1, 1, 1,
1, 1, 1, 1 }, image.get()));
mask.replace(Rect(0, 0, 5, 5));
algorithm::fill_selection(image.get(), Rect(4, 4, 32, 32), &mask, 2, &grid);
EXPECT_TRUE(cmp_img({ 2, 1, 1, 1,
1, 1, 1, 1,
1, 1, 1, 1,
1, 1, 1, 1 }, image.get()));
mask.replace(Rect(12, 12, 9, 8));
algorithm::fill_selection(image.get(), Rect(4, 4, 32, 32), &mask, 3, &grid);
EXPECT_TRUE(cmp_img({ 2, 1, 1, 1,
1, 3, 3, 1,
1, 1, 1, 1,
1, 1, 1, 1 }, image.get()));
}
int main(int argc, char** argv)
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

View File

@ -18,9 +18,10 @@ namespace doc {
namespace algorithm {
void stroke_selection(Image* image,
const gfx::Point& offset,
const gfx::Rect& imageBounds,
const Mask* origMask,
const color_t color)
const color_t color,
const Grid* grid)
{
ASSERT(origMask);
ASSERT(origMask->bitmap());
@ -44,7 +45,7 @@ void stroke_selection(Image* image,
ASSERT(mask.bounds() == origMask->bounds());
if (mask.bitmap())
fill_selection(image, offset, &mask, color);
fill_selection(image, imageBounds, &mask, color, grid);
}
} // namespace algorithm

View File

@ -1,4 +1,5 @@
// Aseprite Document Library
// Copyright (c) 2020 Igara Studio S.A.
// Copyright (c) 2018 David Capello
//
// This file is released under the terms of the MIT license.
@ -9,17 +10,23 @@
#pragma once
#include "doc/color.h"
#include "gfx/point.h"
#include "gfx/rect.h"
namespace doc {
class Grid;
class Image;
class Mask;
namespace algorithm {
void stroke_selection(Image* image,
const gfx::Point& offset,
const Mask* mask,
const color_t color);
void stroke_selection(
Image* image,
const gfx::Rect& imageBounds,
const Mask* mask,
// This can be a color_t or a tile_t if the image is a tilemap
const color_t color,
// Optional grid for tilemaps
const Grid* grid = nullptr);
} // namespace algorithm
} // namespace doc

View File

@ -11,11 +11,13 @@
#include "doc/cel.h"
#include "gfx/rect.h"
#include "doc/grid.h"
#include "doc/image.h"
#include "doc/layer.h"
#include "doc/layer_tilemap.h"
#include "doc/sprite.h"
#include "doc/tile.h"
#include "gfx/rect.h"
namespace doc {
@ -146,6 +148,20 @@ void Cel::setParentLayer(LayerImage* layer)
fixupImage();
}
Grid Cel::grid() const
{
if (m_layer) {
if (m_layer->isTilemap()) {
doc::Grid grid = static_cast<LayerTilemap*>(m_layer)->tileset()->grid();
grid.origin(grid.origin() + position());
return grid;
}
else
return m_layer->grid();
}
return Grid();
}
void Cel::fixupImage()
{
// Change the mask color to the sprite mask color

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (c) 2019 Igara Studio S.A.
// Copyright (c) 2019-2020 Igara Studio S.A.
// Copyright (c) 2001-2016 David Capello
//
// This file is released under the terms of the MIT license.
@ -20,6 +20,7 @@
namespace doc {
class Document;
class Grid;
class LayerImage;
class Sprite;
@ -67,6 +68,7 @@ namespace doc {
}
void setParentLayer(LayerImage* layer);
Grid grid() const;
private:
void fixupImage();

View File

@ -46,6 +46,7 @@ namespace doc {
const ImageSpec& spec() const { return m_spec; }
ColorMode colorMode() const { return m_spec.colorMode(); }
PixelFormat pixelFormat() const { return (PixelFormat)colorMode(); }
bool isTilemap() const { return m_spec.colorMode() == ColorMode::TILEMAP; }
int width() const { return m_spec.width(); }
int height() const { return m_spec.height(); }
gfx::Size size() const { return m_spec.size(); }

View File

@ -12,6 +12,7 @@
#include "doc/layer.h"
#include "doc/cel.h"
#include "doc/grid.h"
#include "doc/image.h"
#include "doc/primitives.h"
#include "doc/sprite.h"
@ -190,6 +191,15 @@ bool Layer::hasAncestor(const Layer* ancestor) const
return false;
}
Grid Layer::grid() const
{
gfx::Rect rc = (m_sprite ? m_sprite->gridBounds():
doc::Sprite::DefaultGridBounds());
doc::Grid grid = Grid(rc.size());
grid.origin(gfx::Point(rc.x % rc.w, rc.y % rc.h));
return grid;
}
Cel* Layer::cel(frame_t frame) const
{
return nullptr;

View File

@ -21,11 +21,12 @@
namespace doc {
class Cel;
class Grid;
class Image;
class Sprite;
class Layer;
class LayerGroup;
class LayerImage;
class Sprite;
//////////////////////////////////////////////////////////////////////
// Layer class
@ -120,6 +121,7 @@ namespace doc {
m_flags = LayerFlags(int(m_flags) & ~int(flags));
}
virtual Grid grid() const;
virtual Cel* cel(frame_t frame) const;
virtual void getCels(CelList& cels) const = 0;
virtual void displaceFrames(frame_t fromThis, frame_t delta) = 0;

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (c) 2019 Igara Studio S.A.
// Copyright (c) 2019-2020 Igara Studio S.A.
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
@ -28,6 +28,14 @@ LayerTilemap::~LayerTilemap()
{
}
Grid LayerTilemap::grid() const
{
if (m_tileset)
return m_tileset->grid();
else
return Layer::grid();
}
void LayerTilemap::setTilesetIndex(tileset_index tsi)
{
m_tilesetIndex = tsi;

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (c) 2019 Igara Studio S.A.
// Copyright (c) 2019-2020 Igara Studio S.A.
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
@ -19,6 +19,8 @@ namespace doc {
explicit LayerTilemap(Sprite* sprite, const tileset_index tsi);
~LayerTilemap();
Grid grid() const override;
// Returns the tileset of this layer. New automatically-created
// tiles should be stored into this tileset, and all tiles in the
// layer should share the same Grid spec.

View File

@ -1,4 +1,5 @@
// Aseprite Document Library
// Copyright (c) 2020 Igara Studio S.A.
// Copyright (c) 2001-2018 David Capello
//
// This file is released under the terms of the MIT license.
@ -50,6 +51,7 @@ namespace doc {
get_pixel(m_bitmap.get(), u-m_bounds.x, v-m_bounds.y));
}
gfx::Point origin() const { return m_bounds.origin(); }
const gfx::Rect& bounds() const { return m_bounds; }
void setOrigin(int x, int y) {