mirror of
https://github.com/aseprite/aseprite.git
synced 2025-03-25 23:37:05 +00:00
886 lines
27 KiB
C++
886 lines
27 KiB
C++
// Aseprite
|
|
// Copyright (C) 2019-2021 Igara Studio S.A.
|
|
// Copyright (C) 2001-2018 David Capello
|
|
//
|
|
// This program is distributed under the terms of
|
|
// the End-User License Agreement for Aseprite.
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include "config.h"
|
|
#endif
|
|
|
|
#include "app/util/cel_ops.h"
|
|
|
|
#include "app/cmd/add_tile.h"
|
|
#include "app/cmd/clear_cel.h"
|
|
#include "app/cmd/clear_mask.h"
|
|
#include "app/cmd/copy_region.h"
|
|
#include "app/cmd/remap_tilemaps.h"
|
|
#include "app/cmd/remap_tileset.h"
|
|
#include "app/cmd/remove_tile.h"
|
|
#include "app/cmd/replace_image.h"
|
|
#include "app/cmd/set_cel_position.h"
|
|
#include "app/cmd_sequence.h"
|
|
#include "app/doc.h"
|
|
#include "doc/algorithm/fill_selection.h"
|
|
#include "doc/algorithm/resize_image.h"
|
|
#include "doc/algorithm/shrink_bounds.h"
|
|
#include "doc/cel.h"
|
|
#include "doc/grid.h"
|
|
#include "doc/image.h"
|
|
#include "doc/layer.h"
|
|
#include "doc/layer_tilemap.h"
|
|
#include "doc/mask.h"
|
|
#include "doc/palette.h"
|
|
#include "doc/primitives.h"
|
|
#include "doc/sprite.h"
|
|
#include "doc/tileset.h"
|
|
#include "doc/tilesets.h"
|
|
#include "gfx/region.h"
|
|
#include "render/dithering.h"
|
|
#include "render/ordered_dither.h"
|
|
#include "render/quantization.h"
|
|
#include "render/render.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <memory>
|
|
#include <vector>
|
|
|
|
#define OPS_TRACE(...) // TRACE(__VA_ARGS__)
|
|
|
|
namespace app {
|
|
|
|
using namespace doc;
|
|
|
|
namespace {
|
|
|
|
template<typename ImageTraits>
|
|
void mask_image_templ(Image* image, const Image* bitmap)
|
|
{
|
|
LockImageBits<ImageTraits> bits1(image);
|
|
const LockImageBits<BitmapTraits> bits2(bitmap);
|
|
typename LockImageBits<ImageTraits>::iterator it1, end1;
|
|
LockImageBits<BitmapTraits>::const_iterator it2, end2;
|
|
for (it1 = bits1.begin(), end1 = bits1.end(),
|
|
it2 = bits2.begin(), end2 = bits2.end();
|
|
it1 != end1 && it2 != end2; ++it1, ++it2) {
|
|
if (!*it2)
|
|
*it1 = image->maskColor();
|
|
}
|
|
ASSERT(it1 == end1);
|
|
ASSERT(it2 == end2);
|
|
}
|
|
|
|
void mask_image(Image* image, Image* bitmap)
|
|
{
|
|
ASSERT(image->bounds() == bitmap->bounds());
|
|
switch (image->pixelFormat()) {
|
|
case IMAGE_RGB: return mask_image_templ<RgbTraits>(image, bitmap);
|
|
case IMAGE_GRAYSCALE: return mask_image_templ<GrayscaleTraits>(image, bitmap);
|
|
case IMAGE_INDEXED: return mask_image_templ<IndexedTraits>(image, bitmap);
|
|
}
|
|
}
|
|
|
|
template<typename ImageTraits>
|
|
void create_region_with_differences_templ(const Image* a,
|
|
const Image* b,
|
|
const gfx::Rect& bounds,
|
|
gfx::Region& output)
|
|
{
|
|
for (int y=bounds.y; y<bounds.y2(); ++y) {
|
|
for (int x=bounds.x; x<bounds.x2(); ++x) {
|
|
if (get_pixel_fast<ImageTraits>(a, x, y) !=
|
|
get_pixel_fast<ImageTraits>(b, x, y)) {
|
|
output.createUnion(output, gfx::Region(gfx::Rect(x, y, 1, 1)));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO merge this with Sprite::getTilemapsByTileset()
|
|
template<typename UnaryFunction>
|
|
void for_each_tile_using_tileset(Tileset* tileset, UnaryFunction f)
|
|
{
|
|
for (Cel* cel : tileset->sprite()->cels()) {
|
|
if (!cel->layer()->isTilemap() ||
|
|
static_cast<LayerTilemap*>(cel->layer())->tileset() != tileset)
|
|
continue;
|
|
|
|
Image* tilemapImage = cel->image();
|
|
for_each_pixel<TilemapTraits>(tilemapImage, f);
|
|
}
|
|
}
|
|
|
|
struct Mod {
|
|
tile_index tileIndex;
|
|
ImageRef tileDstImage;
|
|
ImageRef tileImage;
|
|
gfx::Region tileRgn;
|
|
};
|
|
|
|
} // anonymous namespace
|
|
|
|
void create_region_with_differences(const Image* a,
|
|
const Image* b,
|
|
const gfx::Rect& bounds,
|
|
gfx::Region& output)
|
|
{
|
|
ASSERT(a->pixelFormat() == b->pixelFormat());
|
|
switch (a->pixelFormat()) {
|
|
case IMAGE_RGB: create_region_with_differences_templ<RgbTraits>(a, b, bounds, output); break;
|
|
case IMAGE_GRAYSCALE: create_region_with_differences_templ<GrayscaleTraits>(a, b, bounds, output); break;
|
|
case IMAGE_INDEXED: create_region_with_differences_templ<IndexedTraits>(a, b, bounds, output); break;
|
|
}
|
|
}
|
|
|
|
static void remove_unused_tiles_from_tileset(
|
|
CmdSequence* cmds,
|
|
doc::Tileset* tileset,
|
|
std::vector<size_t>& tilesHistogram,
|
|
const std::vector<bool>& modifiedTileIndexes);
|
|
|
|
doc::ImageRef crop_cel_image(
|
|
const doc::Cel* cel,
|
|
const color_t bgcolor)
|
|
{
|
|
doc::Sprite* sprite = cel->sprite();
|
|
|
|
if (cel->layer()->isTilemap()) {
|
|
doc::ImageRef dstImage(doc::Image::create(sprite->spec()));
|
|
|
|
render::Render().renderCel(
|
|
dstImage.get(),
|
|
cel,
|
|
sprite,
|
|
cel->image(),
|
|
cel->layer(),
|
|
sprite->palette(cel->frame()),
|
|
dstImage->bounds(),
|
|
gfx::Clip(cel->position(), dstImage->bounds()),
|
|
255, BlendMode::NORMAL);
|
|
|
|
return dstImage;
|
|
}
|
|
else {
|
|
return doc::ImageRef(
|
|
doc::crop_image(
|
|
cel->image(),
|
|
gfx::Rect(sprite->bounds()).offset(-cel->position()),
|
|
bgcolor));
|
|
}
|
|
}
|
|
|
|
Cel* create_cel_copy(CmdSequence* cmds,
|
|
const Cel* srcCel,
|
|
const Sprite* dstSprite,
|
|
Layer* dstLayer,
|
|
const frame_t dstFrame)
|
|
{
|
|
const Image* srcImage = srcCel->image();
|
|
doc::PixelFormat dstPixelFormat =
|
|
(dstLayer->isTilemap() ? IMAGE_TILEMAP:
|
|
dstSprite->pixelFormat());
|
|
gfx::Size dstSize(srcImage->width(),
|
|
srcImage->height());
|
|
|
|
// From Tilemap -> Image
|
|
if (srcCel->layer()->isTilemap() && !dstLayer->isTilemap()) {
|
|
auto layerTilemap = static_cast<doc::LayerTilemap*>(srcCel->layer());
|
|
dstSize = layerTilemap->tileset()->grid().tilemapSizeToCanvas(dstSize);
|
|
}
|
|
// From Image or Tilemap -> Tilemap
|
|
else if (dstLayer->isTilemap()) {
|
|
auto dstLayerTilemap = static_cast<doc::LayerTilemap*>(dstLayer);
|
|
|
|
// Tilemap -> Tilemap
|
|
Grid grid;
|
|
if (srcCel->layer()->isTilemap()) {
|
|
grid = dstLayerTilemap->tileset()->grid();
|
|
if (srcCel->layer()->isTilemap())
|
|
grid.origin(srcCel->position());
|
|
}
|
|
// Image -> Tilemap
|
|
else {
|
|
auto gridBounds = dstLayerTilemap->sprite()->gridBounds();
|
|
grid.origin(gridBounds.origin());
|
|
grid.tileSize(gridBounds.size());
|
|
}
|
|
|
|
const gfx::Rect tilemapBounds = grid.canvasToTile(srcCel->bounds());
|
|
dstSize = tilemapBounds.size();
|
|
}
|
|
|
|
// New cel
|
|
std::unique_ptr<Cel> dstCel(
|
|
new Cel(dstFrame, ImageRef(Image::create(dstPixelFormat, dstSize.w, dstSize.h))));
|
|
|
|
dstCel->setOpacity(srcCel->opacity());
|
|
dstCel->data()->setUserData(srcCel->data()->userData());
|
|
|
|
// Special case were we copy from a tilemap...
|
|
if (srcCel->layer()->isTilemap()) {
|
|
if (dstLayer->isTilemap()) {
|
|
// Tilemap -> Tilemap (with same tileset)
|
|
// Best case, copy a cel in the same layer (we have the same
|
|
// tileset available, so we just copy the tilemap as it is).
|
|
if (srcCel->layer() == dstLayer) {
|
|
dstCel->image()->copy(srcImage, gfx::Clip(0, 0, srcImage->bounds()));
|
|
}
|
|
// Tilemap -> Tilemap (with different tilesets)
|
|
else {
|
|
doc::ImageSpec spec = dstSprite->spec();
|
|
spec.setSize(srcCel->bounds().size());
|
|
doc::ImageRef tmpImage(doc::Image::create(spec));
|
|
render::Render().renderCel(
|
|
tmpImage.get(),
|
|
srcCel,
|
|
dstSprite,
|
|
srcImage,
|
|
srcCel->layer(),
|
|
dstSprite->palette(dstCel->frame()),
|
|
gfx::Rect(gfx::Point(0, 0), srcCel->bounds().size()),
|
|
gfx::Clip(0, 0, tmpImage->bounds()),
|
|
255, BlendMode::NORMAL);
|
|
|
|
doc::ImageRef tilemap = dstCel->imageRef();
|
|
|
|
draw_image_into_new_tilemap_cel(
|
|
cmds, static_cast<doc::LayerTilemap*>(dstLayer), dstCel.get(),
|
|
tmpImage.get(),
|
|
srcCel->bounds().origin(),
|
|
srcCel->bounds().origin(),
|
|
srcCel->bounds(),
|
|
tilemap);
|
|
}
|
|
dstCel->setPosition(srcCel->position());
|
|
}
|
|
// Tilemap -> Image (so we convert the tilemap to a regular image)
|
|
else {
|
|
render::Render().renderCel(
|
|
dstCel->image(),
|
|
srcCel,
|
|
dstSprite,
|
|
srcImage,
|
|
srcCel->layer(),
|
|
dstSprite->palette(dstCel->frame()),
|
|
gfx::Rect(gfx::Point(0, 0), srcCel->bounds().size()),
|
|
gfx::Clip(0, 0, dstCel->image()->bounds()),
|
|
255, BlendMode::NORMAL);
|
|
|
|
// Shrink image
|
|
if (dstLayer->isTransparent()) {
|
|
auto bg = dstCel->image()->maskColor();
|
|
gfx::Rect bounds;
|
|
if (algorithm::shrink_bounds(dstCel->image(), bg, dstLayer, bounds)) {
|
|
ImageRef trimmed(doc::crop_image(dstCel->image(), bounds, bg));
|
|
dstCel->data()->setImage(trimmed, dstLayer);
|
|
dstCel->setPosition(srcCel->position() + bounds.origin());
|
|
return dstCel.release();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Image -> Tilemap (we'll need to generate new tilesets)
|
|
else if (dstLayer->isTilemap()) {
|
|
doc::ImageRef tilemap = dstCel->imageRef();
|
|
draw_image_into_new_tilemap_cel(
|
|
cmds, static_cast<doc::LayerTilemap*>(dstLayer), dstCel.get(),
|
|
srcImage,
|
|
// Use the grid origin of the sprite
|
|
srcCel->sprite()->gridBounds().origin(),
|
|
srcCel->bounds().origin(),
|
|
srcCel->bounds(),
|
|
tilemap);
|
|
}
|
|
else if ((dstSprite->pixelFormat() != srcImage->pixelFormat()) ||
|
|
// If both images are indexed but with different palette, we can
|
|
// convert the source cel to RGB first.
|
|
(dstSprite->pixelFormat() == IMAGE_INDEXED &&
|
|
srcImage->pixelFormat() == IMAGE_INDEXED &&
|
|
srcCel->sprite()->palette(srcCel->frame())->countDiff(
|
|
dstSprite->palette(dstFrame), nullptr, nullptr))) {
|
|
ImageRef tmpImage(Image::create(IMAGE_RGB, srcImage->width(), srcImage->height()));
|
|
tmpImage->clear(0);
|
|
|
|
render::convert_pixel_format(
|
|
srcImage,
|
|
tmpImage.get(),
|
|
IMAGE_RGB,
|
|
render::Dithering(),
|
|
srcCel->sprite()->rgbMap(srcCel->frame()),
|
|
srcCel->sprite()->palette(srcCel->frame()),
|
|
srcCel->layer()->isBackground(),
|
|
0);
|
|
|
|
render::convert_pixel_format(
|
|
tmpImage.get(),
|
|
dstCel->image(),
|
|
IMAGE_INDEXED,
|
|
render::Dithering(),
|
|
dstSprite->rgbMap(dstFrame),
|
|
dstSprite->palette(dstFrame),
|
|
srcCel->layer()->isBackground(),
|
|
dstSprite->transparentColor());
|
|
}
|
|
// Simple case, where we copy both images
|
|
else {
|
|
render::composite_image(
|
|
dstCel->image(),
|
|
srcImage,
|
|
srcCel->sprite()->palette(srcCel->frame()),
|
|
0, 0, 255, BlendMode::SRC);
|
|
}
|
|
|
|
// Resize a referece cel to a non-reference layer
|
|
if (srcCel->layer()->isReference() && !dstLayer->isReference()) {
|
|
gfx::RectF srcBounds = srcCel->boundsF();
|
|
|
|
std::unique_ptr<Cel> dstCel2(
|
|
new Cel(dstFrame,
|
|
ImageRef(Image::create(dstSprite->pixelFormat(),
|
|
std::ceil(srcBounds.w),
|
|
std::ceil(srcBounds.h)))));
|
|
algorithm::resize_image(
|
|
dstCel->image(), dstCel2->image(),
|
|
algorithm::RESIZE_METHOD_NEAREST_NEIGHBOR,
|
|
nullptr, nullptr, 0);
|
|
|
|
dstCel.reset(dstCel2.release());
|
|
dstCel->setPosition(gfx::Point(srcBounds.origin()));
|
|
}
|
|
// Copy original cel bounds
|
|
else if (!dstLayer->isTilemap()) {
|
|
if (srcCel->layer() &&
|
|
srcCel->layer()->isReference()) {
|
|
dstCel->setBoundsF(srcCel->boundsF());
|
|
}
|
|
else {
|
|
dstCel->setPosition(srcCel->position());
|
|
}
|
|
}
|
|
|
|
return dstCel.release();
|
|
}
|
|
|
|
void draw_image_into_new_tilemap_cel(
|
|
CmdSequence* cmds,
|
|
doc::LayerTilemap* dstLayer,
|
|
doc::Cel* dstCel,
|
|
const doc::Image* srcImage,
|
|
const gfx::Point& gridOrigin,
|
|
const gfx::Point& srcImagePos,
|
|
const gfx::Rect& canvasBounds,
|
|
doc::ImageRef& newTilemap)
|
|
{
|
|
ASSERT(dstLayer->isTilemap());
|
|
|
|
doc::Tileset* tileset = dstLayer->tileset();
|
|
doc::Grid grid = tileset->grid();
|
|
grid.origin(gridOrigin);
|
|
|
|
gfx::Size tileSize = grid.tileSize();
|
|
const gfx::Rect tilemapBounds = grid.canvasToTile(canvasBounds);
|
|
|
|
if (!newTilemap) {
|
|
newTilemap.reset(doc::Image::create(IMAGE_TILEMAP,
|
|
tilemapBounds.w,
|
|
tilemapBounds.h));
|
|
newTilemap->setMaskColor(doc::notile);
|
|
newTilemap->clear(doc::notile);
|
|
}
|
|
else {
|
|
ASSERT(tilemapBounds.w == newTilemap->width());
|
|
ASSERT(tilemapBounds.h == newTilemap->height());
|
|
}
|
|
|
|
for (const gfx::Point& tilePt : grid.tilesInCanvasRegion(gfx::Region(canvasBounds))) {
|
|
const gfx::Point tilePtInCanvas = grid.tileToCanvas(tilePt);
|
|
doc::ImageRef tileImage(
|
|
doc::crop_image(srcImage,
|
|
tilePtInCanvas.x-srcImagePos.x,
|
|
tilePtInCanvas.y-srcImagePos.y,
|
|
tileSize.w, tileSize.h,
|
|
srcImage->maskColor()));
|
|
if (grid.hasMask())
|
|
mask_image(tileImage.get(), grid.mask().get());
|
|
|
|
preprocess_transparent_pixels(tileImage.get());
|
|
|
|
doc::tile_index tileIndex;
|
|
if (!tileset->findTileIndex(tileImage, tileIndex)) {
|
|
auto addTile = new cmd::AddTile(tileset, tileImage);
|
|
|
|
if (cmds)
|
|
cmds->executeAndAdd(addTile);
|
|
else {
|
|
// TODO a little hacky
|
|
addTile->execute(
|
|
static_cast<Doc*>(dstLayer->sprite()->document())->context());
|
|
}
|
|
|
|
tileIndex = addTile->tileIndex();
|
|
|
|
if (!cmds)
|
|
delete addTile;
|
|
}
|
|
|
|
newTilemap->putPixel(
|
|
tilePt.x-tilemapBounds.x,
|
|
tilePt.y-tilemapBounds.y, tileIndex);
|
|
}
|
|
|
|
static_cast<Doc*>(dstLayer->sprite()->document())
|
|
->notifyTilesetChanged(tileset);
|
|
|
|
dstCel->data()->setImage(newTilemap, dstLayer);
|
|
dstCel->setPosition(grid.tileToCanvas(tilemapBounds.origin()));
|
|
}
|
|
|
|
void modify_tilemap_cel_region(
|
|
CmdSequence* cmds,
|
|
doc::Cel* cel,
|
|
doc::Tileset* tileset,
|
|
const gfx::Region& region,
|
|
const TilesetMode tilesetMode,
|
|
const GetTileImageFunc& getTileImage)
|
|
{
|
|
OPS_TRACE("modify_tilemap_cel_region %d %d %d %d\n",
|
|
region.bounds().x, region.bounds().y,
|
|
region.bounds().w, region.bounds().h);
|
|
|
|
if (region.isEmpty())
|
|
return;
|
|
|
|
ASSERT(cel->layer() && cel->layer()->isTilemap());
|
|
ASSERT(cel->image()->pixelFormat() == IMAGE_TILEMAP);
|
|
|
|
doc::LayerTilemap* tilemapLayer = static_cast<doc::LayerTilemap*>(cel->layer());
|
|
|
|
Doc* doc = static_cast<Doc*>(tilemapLayer->sprite()->document());
|
|
bool addUndoToTileset = false;
|
|
if (!tileset) {
|
|
tileset = tilemapLayer->tileset();
|
|
addUndoToTileset = true;
|
|
}
|
|
doc::Grid grid = tileset->grid();
|
|
grid.origin(grid.origin() + cel->position());
|
|
|
|
const gfx::Size tileSize = grid.tileSize();
|
|
const gfx::Rect oldTilemapBounds(grid.canvasToTile(cel->position()),
|
|
cel->image()->bounds().size());
|
|
const gfx::Rect patchTilemapBounds = grid.canvasToTile(region.bounds());
|
|
const gfx::Rect newTilemapBounds = (oldTilemapBounds | patchTilemapBounds);
|
|
|
|
OPS_TRACE("modify_tilemap_cel_region:\n"
|
|
" - grid.origin =%d %d\n"
|
|
" - cel.position =%d %d\n"
|
|
" - oldTilemapBounds =%d %d %d %d\n"
|
|
" - patchTilemapBounds=%d %d %d %d (region.bounds = %d %d %d %d)\n"
|
|
" - newTilemapBounds =%d %d %d %d\n",
|
|
grid.origin().x, grid.origin().y,
|
|
cel->position().x, cel->position().y,
|
|
oldTilemapBounds.x, oldTilemapBounds.y, oldTilemapBounds.w, oldTilemapBounds.h,
|
|
patchTilemapBounds.x, patchTilemapBounds.y, patchTilemapBounds.w, patchTilemapBounds.h,
|
|
region.bounds().x, region.bounds().y, region.bounds().w, region.bounds().h,
|
|
newTilemapBounds.x, newTilemapBounds.y, newTilemapBounds.w, newTilemapBounds.h);
|
|
|
|
// Autogenerate tiles
|
|
if (tilesetMode == TilesetMode::Auto ||
|
|
tilesetMode == TilesetMode::Stack) {
|
|
// TODO create a smaller image
|
|
doc::ImageRef newTilemap(
|
|
doc::Image::create(IMAGE_TILEMAP,
|
|
newTilemapBounds.w,
|
|
newTilemapBounds.h));
|
|
|
|
newTilemap->setMaskColor(doc::notile);
|
|
newTilemap->clear(doc::notile); // TODO find the tile with empty content?
|
|
newTilemap->copy(
|
|
cel->image(),
|
|
gfx::Clip(oldTilemapBounds.x-newTilemapBounds.x,
|
|
oldTilemapBounds.y-newTilemapBounds.y, 0, 0,
|
|
oldTilemapBounds.w, oldTilemapBounds.h));
|
|
|
|
gfx::Region tilePtsRgn;
|
|
|
|
// This region includes the modified region by the user + the
|
|
// extra region added as we've incremented the tilemap size
|
|
// (newTilemapBounds).
|
|
gfx::Region regionToPatch(grid.tileToCanvas(newTilemapBounds));
|
|
regionToPatch -= gfx::Region(grid.tileToCanvas(oldTilemapBounds));
|
|
regionToPatch |= region;
|
|
|
|
std::vector<bool> modifiedTileIndexes(tileset->size(), false);
|
|
std::vector<size_t> tilesHistogram(tileset->size(), 0);
|
|
if (tilesetMode == TilesetMode::Auto) {
|
|
for_each_tile_using_tileset(
|
|
tileset, [tileset, &tilesHistogram](const doc::tile_t t){
|
|
if (t != doc::notile) {
|
|
doc::tile_index ti = doc::tile_geti(t);
|
|
if (ti >= 0 && ti < tileset->size())
|
|
++tilesHistogram[ti];
|
|
}
|
|
});
|
|
}
|
|
|
|
for (const gfx::Point& tilePt : grid.tilesInCanvasRegion(regionToPatch)) {
|
|
const int u = tilePt.x-newTilemapBounds.x;
|
|
const int v = tilePt.y-newTilemapBounds.y;
|
|
OPS_TRACE(" - modify tile xy=%d %d uv=%d %d\n", tilePt.x, tilePt.y, u, v);
|
|
if (!newTilemap->bounds().contains(u, v))
|
|
continue;
|
|
|
|
const doc::tile_t t = newTilemap->getPixel(u, v);
|
|
const doc::tile_index ti = (t != doc::notile ? doc::tile_geti(t): doc::notile);
|
|
const doc::ImageRef existentTileImage = tileset->get(ti);
|
|
|
|
const gfx::Rect tileInCanvasRc(grid.tileToCanvas(tilePt), tileSize);
|
|
ImageRef tileImage(getTileImage(existentTileImage, tileInCanvasRc));
|
|
if (grid.hasMask())
|
|
mask_image(tileImage.get(), grid.mask().get());
|
|
|
|
preprocess_transparent_pixels(tileImage.get());
|
|
|
|
tile_index tileIndex;
|
|
if (tileset->findTileIndex(tileImage, tileIndex)) {
|
|
// We can re-use an existent tile (tileIndex) from the tileset
|
|
}
|
|
else if (tilesetMode == TilesetMode::Auto &&
|
|
t != doc::notile &&
|
|
ti >= 0 && ti < tilesHistogram.size() &&
|
|
tilesHistogram[ti] == 1) {
|
|
// Common case: Re-utilize the same tile in Auto mode.
|
|
tileIndex = ti;
|
|
cmds->executeAndAdd(
|
|
new cmd::CopyTileRegion(
|
|
existentTileImage.get(),
|
|
tileImage.get(),
|
|
gfx::Region(tileImage->bounds()), // TODO calculate better region
|
|
gfx::Point(0, 0),
|
|
false,
|
|
tileIndex,
|
|
tileset));
|
|
}
|
|
else {
|
|
auto addTile = new cmd::AddTile(tileset, tileImage);
|
|
cmds->executeAndAdd(addTile);
|
|
|
|
tileIndex = addTile->tileIndex();
|
|
}
|
|
|
|
// If the tile changed, we have to remove the old tile index
|
|
// (ti) from the histogram count.
|
|
if (tilesetMode == TilesetMode::Auto &&
|
|
t != doc::notile &&
|
|
ti >= 0 && ti < tilesHistogram.size() &&
|
|
ti != tileIndex) {
|
|
--tilesHistogram[ti];
|
|
|
|
if (tilesetMode == TilesetMode::Auto)
|
|
modifiedTileIndexes[ti] = true;
|
|
}
|
|
|
|
OPS_TRACE(" - tile %d -> %d\n",
|
|
(t == doc::notile ? -1: ti),
|
|
tileIndex);
|
|
|
|
const doc::tile_t tile = doc::tile(tileIndex, 0);
|
|
if (t != tile) {
|
|
newTilemap->putPixel(u, v, tile);
|
|
tilePtsRgn |= gfx::Region(gfx::Rect(u, v, 1, 1));
|
|
}
|
|
}
|
|
|
|
if (newTilemap->width() != cel->image()->width() ||
|
|
newTilemap->height() != cel->image()->height()) {
|
|
gfx::Point newPos = grid.tileToCanvas(newTilemapBounds.origin());
|
|
if (cel->position() != newPos) {
|
|
cmds->executeAndAdd(
|
|
new cmd::SetCelPosition(cel, newPos.x, newPos.y));
|
|
}
|
|
cmds->executeAndAdd(
|
|
new cmd::ReplaceImage(cel->sprite(), cel->imageRef(), newTilemap));
|
|
}
|
|
else if (!tilePtsRgn.isEmpty()) {
|
|
cmds->executeAndAdd(
|
|
new cmd::CopyRegion(
|
|
cel->image(),
|
|
newTilemap.get(),
|
|
tilePtsRgn,
|
|
gfx::Point(0, 0)));
|
|
}
|
|
|
|
// Remove unused tiles
|
|
if (tilesetMode == TilesetMode::Auto) {
|
|
remove_unused_tiles_from_tileset(cmds, tileset,
|
|
tilesHistogram,
|
|
modifiedTileIndexes);
|
|
}
|
|
|
|
doc->notifyTilesetChanged(tileset);
|
|
}
|
|
// Modify active set of tiles manually / don't auto-generate new tiles
|
|
else if (tilesetMode == TilesetMode::Manual) {
|
|
std::vector<Mod> mods;
|
|
|
|
for (const gfx::Point& tilePt : grid.tilesInCanvasRegion(region)) {
|
|
// Ignore modifications outside the tilemap
|
|
if (!cel->image()->bounds().contains(tilePt.x, tilePt.y))
|
|
continue;
|
|
|
|
const doc::tile_t t = cel->image()->getPixel(tilePt.x, tilePt.y);
|
|
if (t == doc::notile)
|
|
continue;
|
|
|
|
const doc::tile_index ti = doc::tile_geti(t);
|
|
const doc::ImageRef existentTileImage = tileset->get(ti);
|
|
if (!existentTileImage) {
|
|
// TODO add support to fill the tileset with the tile "ti"
|
|
continue;
|
|
}
|
|
|
|
const gfx::Rect tileInCanvasRc(grid.tileToCanvas(tilePt), tileSize);
|
|
ImageRef tileImage(getTileImage(existentTileImage, tileInCanvasRc));
|
|
if (grid.hasMask())
|
|
mask_image(tileImage.get(), grid.mask().get());
|
|
|
|
gfx::Region tileRgn(tileInCanvasRc);
|
|
tileRgn.createIntersection(tileRgn, region);
|
|
tileRgn.offset(-tileInCanvasRc.origin());
|
|
|
|
ImageRef tileDstImage = tileset->get(ti);
|
|
|
|
// Compare with the original tile from the original tileset
|
|
gfx::Region diffRgn;
|
|
create_region_with_differences(tilemapLayer->tileset()->get(ti).get(),
|
|
tileImage.get(),
|
|
tileRgn.bounds(),
|
|
diffRgn);
|
|
|
|
// Keep only the modified region for this specific modification
|
|
tileRgn &= diffRgn;
|
|
|
|
if (!tileRgn.isEmpty()) {
|
|
if (addUndoToTileset) {
|
|
Mod mod;
|
|
mod.tileIndex = ti;
|
|
mod.tileDstImage = tileDstImage;
|
|
mod.tileImage = tileImage;
|
|
mod.tileRgn = tileRgn;
|
|
mods.push_back(mod);
|
|
}
|
|
else {
|
|
copy_image(tileDstImage.get(),
|
|
tileImage.get(),
|
|
tileRgn);
|
|
tileset->notifyTileContentChange(ti);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply all modifications to tiles
|
|
if (addUndoToTileset) {
|
|
for (auto& mod : mods) {
|
|
// TODO avoid creating several CopyTileRegion for the same tile,
|
|
// merge all mods for the same tile in some way
|
|
cmds->executeAndAdd(
|
|
new cmd::CopyTileRegion(
|
|
mod.tileDstImage.get(),
|
|
mod.tileImage.get(),
|
|
mod.tileRgn,
|
|
gfx::Point(0, 0),
|
|
false,
|
|
mod.tileIndex,
|
|
tileset));
|
|
}
|
|
}
|
|
|
|
doc->notifyTilesetChanged(tileset);
|
|
}
|
|
|
|
#ifdef _DEBUG
|
|
tileset->assertValidHashTable();
|
|
#endif
|
|
}
|
|
|
|
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() && tilemapMode == TilemapMode::Pixels) {
|
|
Doc* doc = static_cast<Doc*>(cel->document());
|
|
|
|
// Simple case (there is no visible selection, so we remove the
|
|
// whole cel)
|
|
if (!doc->isMaskVisible()) {
|
|
cmds->executeAndAdd(new cmd::ClearCel(cel));
|
|
return;
|
|
}
|
|
|
|
color_t bgcolor = doc->bgColor(cel->layer());
|
|
doc::Mask* mask = doc->mask();
|
|
|
|
modify_tilemap_cel_region(
|
|
cmds, cel, nullptr,
|
|
gfx::Region(doc->mask()->bounds()),
|
|
tilesetMode,
|
|
[bgcolor, mask](const doc::ImageRef& origTile,
|
|
const gfx::Rect& tileBoundsInCanvas) -> doc::ImageRef {
|
|
doc::ImageRef modified(doc::Image::createCopy(origTile.get()));
|
|
doc::algorithm::fill_selection(
|
|
modified.get(),
|
|
tileBoundsInCanvas,
|
|
mask,
|
|
bgcolor,
|
|
nullptr);
|
|
return modified;
|
|
});
|
|
}
|
|
else {
|
|
cmds->executeAndAdd(new cmd::ClearMask(cel));
|
|
}
|
|
}
|
|
|
|
static void remove_unused_tiles_from_tileset(
|
|
CmdSequence* cmds,
|
|
doc::Tileset* tileset,
|
|
std::vector<size_t>& tilesHistogram,
|
|
const std::vector<bool>& modifiedTileIndexes)
|
|
{
|
|
OPS_TRACE("remove_unused_tiles_from_tileset\n");
|
|
|
|
int n = tileset->size();
|
|
|
|
for_each_tile_using_tileset(
|
|
tileset,
|
|
[&n](const doc::tile_t t){
|
|
if (t != doc::notile) {
|
|
const doc::tile_index ti = doc::tile_geti(t);
|
|
n = std::max<int>(n, ti+1);
|
|
}
|
|
});
|
|
|
|
doc::Remap remap(n);
|
|
doc::tile_index ti, tj;
|
|
ti = tj = 0;
|
|
for (; ti<remap.size(); ++ti) {
|
|
OPS_TRACE(" - ti=%d tj=%d tilesHistogram[%d]=%d\n",
|
|
ti, tj, ti, (ti < tilesHistogram.size() ? tilesHistogram[ti]: 0));
|
|
if (ti < tilesHistogram.size() &&
|
|
tilesHistogram[ti] == 0 &&
|
|
modifiedTileIndexes[ti]) {
|
|
cmds->executeAndAdd(new cmd::RemoveTile(tileset, tj));
|
|
// Map to nothing, so the map can be invertible
|
|
remap.map(ti, doc::Remap::kNoMap);
|
|
}
|
|
else {
|
|
remap.map(ti, tj++);
|
|
}
|
|
}
|
|
|
|
if (!remap.isIdentity()) {
|
|
#ifdef _DEBUG
|
|
for (ti=0; ti<remap.size(); ++ti) {
|
|
OPS_TRACE(" - remap tile[%d] -> %d\n", ti, remap[ti]);
|
|
}
|
|
#endif
|
|
cmds->executeAndAdd(new cmd::RemapTilemaps(tileset, remap));
|
|
}
|
|
}
|
|
|
|
void move_tiles_in_tileset(
|
|
CmdSequence* cmds,
|
|
doc::Tileset* tileset,
|
|
doc::PalettePicks& picks,
|
|
int& currentEntry,
|
|
int beforeIndex)
|
|
{
|
|
OPS_TRACE("move_tiles_in_tileset\n");
|
|
|
|
// We cannot move the empty tile (index 0) no any place
|
|
if (beforeIndex == 0)
|
|
++beforeIndex;
|
|
if (picks.size() > 0 && picks[0])
|
|
picks[0] = false;
|
|
if (!picks.picks())
|
|
return;
|
|
|
|
picks.resize(std::max<int>(picks.size(), beforeIndex));
|
|
|
|
int n = beforeIndex - tileset->size();
|
|
if (n > 0) {
|
|
while (n-- > 0)
|
|
cmds->executeAndAdd(new cmd::AddTile(tileset, tileset->makeEmptyTile()));
|
|
}
|
|
|
|
Remap remap = create_remap_to_move_picks(picks, beforeIndex);
|
|
cmds->executeAndAdd(new cmd::RemapTileset(tileset, remap));
|
|
|
|
// New selection
|
|
auto oldPicks = picks;
|
|
for (int i=0; i<picks.size(); ++i)
|
|
picks[remap[i]] = oldPicks[i];
|
|
currentEntry = remap[currentEntry];
|
|
}
|
|
|
|
void copy_tiles_in_tileset(
|
|
CmdSequence* cmds,
|
|
doc::Tileset* tileset,
|
|
doc::PalettePicks& picks,
|
|
int& currentEntry,
|
|
int beforeIndex)
|
|
{
|
|
// We cannot move tiles before the empty tile
|
|
if (beforeIndex == 0)
|
|
++beforeIndex;
|
|
|
|
OPS_TRACE("copy_tiles_in_tileset beforeIndex=%d npicks=%d\n", beforeIndex, picks.picks());
|
|
|
|
std::vector<ImageRef> newTiles;
|
|
for (int i=0; i<picks.size(); ++i) {
|
|
if (!picks[i])
|
|
continue;
|
|
else if (i >= 0 && i < tileset->size()) {
|
|
newTiles.emplace_back(Image::createCopy(tileset->get(i).get()));
|
|
}
|
|
else {
|
|
newTiles.emplace_back(tileset->makeEmptyTile());
|
|
}
|
|
}
|
|
|
|
int n;
|
|
if (beforeIndex >= picks.size()) {
|
|
n = beforeIndex;
|
|
picks.resize(n);
|
|
}
|
|
else {
|
|
n = tileset->size();
|
|
}
|
|
|
|
const int npicks = picks.picks();
|
|
const int m = n + npicks;
|
|
int j = 0;
|
|
picks.resize(m);
|
|
ASSERT(newTiles.size() == npicks);
|
|
for (int i=0; i<m; ++i) {
|
|
picks[i] = (i >= beforeIndex && i < beforeIndex + npicks);
|
|
if (picks[i]) {
|
|
// Fill the gap between the end of the tileset and the
|
|
// "beforeIndex" with empty tiles
|
|
while (tileset->size() < i)
|
|
cmds->executeAndAdd(new cmd::AddTile(tileset, tileset->makeEmptyTile()));
|
|
|
|
tileset->insert(i, newTiles[j++]);
|
|
cmds->executeAndAdd(new cmd::AddTile(tileset, i));
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace app
|