| 1 | // Aseprite |
| 2 | // Copyright (C) 2019-2021 Igara Studio S.A. |
| 3 | // Copyright (C) 2001-2018 David Capello |
| 4 | // |
| 5 | // This program is distributed under the terms of |
| 6 | // the End-User License Agreement for Aseprite. |
| 7 | |
| 8 | #ifdef HAVE_CONFIG_H |
| 9 | #include "config.h" |
| 10 | #endif |
| 11 | |
| 12 | #include "app/util/cel_ops.h" |
| 13 | |
| 14 | #include "app/cmd/add_tile.h" |
| 15 | #include "app/cmd/clear_cel.h" |
| 16 | #include "app/cmd/clear_mask.h" |
| 17 | #include "app/cmd/copy_region.h" |
| 18 | #include "app/cmd/remap_tilemaps.h" |
| 19 | #include "app/cmd/remap_tileset.h" |
| 20 | #include "app/cmd/remove_tile.h" |
| 21 | #include "app/cmd/replace_image.h" |
| 22 | #include "app/cmd/set_cel_position.h" |
| 23 | #include "app/cmd_sequence.h" |
| 24 | #include "app/doc.h" |
| 25 | #include "doc/algorithm/fill_selection.h" |
| 26 | #include "doc/algorithm/resize_image.h" |
| 27 | #include "doc/algorithm/shrink_bounds.h" |
| 28 | #include "doc/cel.h" |
| 29 | #include "doc/grid.h" |
| 30 | #include "doc/image.h" |
| 31 | #include "doc/layer.h" |
| 32 | #include "doc/layer_tilemap.h" |
| 33 | #include "doc/mask.h" |
| 34 | #include "doc/palette.h" |
| 35 | #include "doc/primitives.h" |
| 36 | #include "doc/sprite.h" |
| 37 | #include "doc/tileset.h" |
| 38 | #include "doc/tilesets.h" |
| 39 | #include "gfx/region.h" |
| 40 | #include "render/dithering.h" |
| 41 | #include "render/ordered_dither.h" |
| 42 | #include "render/quantization.h" |
| 43 | #include "render/render.h" |
| 44 | |
| 45 | #include <algorithm> |
| 46 | #include <cmath> |
| 47 | #include <memory> |
| 48 | #include <vector> |
| 49 | |
| 50 | #define OPS_TRACE(...) // TRACE(__VA_ARGS__) |
| 51 | |
| 52 | namespace app { |
| 53 | |
| 54 | using namespace doc; |
| 55 | |
| 56 | namespace { |
| 57 | |
| 58 | template<typename ImageTraits> |
| 59 | void mask_image_templ(Image* image, const Image* bitmap) |
| 60 | { |
| 61 | LockImageBits<ImageTraits> bits1(image); |
| 62 | const LockImageBits<BitmapTraits> bits2(bitmap); |
| 63 | typename LockImageBits<ImageTraits>::iterator it1, end1; |
| 64 | LockImageBits<BitmapTraits>::const_iterator it2, end2; |
| 65 | for (it1 = bits1.begin(), end1 = bits1.end(), |
| 66 | it2 = bits2.begin(), end2 = bits2.end(); |
| 67 | it1 != end1 && it2 != end2; ++it1, ++it2) { |
| 68 | if (!*it2) |
| 69 | *it1 = image->maskColor(); |
| 70 | } |
| 71 | ASSERT(it1 == end1); |
| 72 | ASSERT(it2 == end2); |
| 73 | } |
| 74 | |
| 75 | void mask_image(Image* image, Image* bitmap) |
| 76 | { |
| 77 | ASSERT(image->bounds() == bitmap->bounds()); |
| 78 | switch (image->pixelFormat()) { |
| 79 | case IMAGE_RGB: return mask_image_templ<RgbTraits>(image, bitmap); |
| 80 | case IMAGE_GRAYSCALE: return mask_image_templ<GrayscaleTraits>(image, bitmap); |
| 81 | case IMAGE_INDEXED: return mask_image_templ<IndexedTraits>(image, bitmap); |
| 82 | } |
| 83 | } |
| 84 | |
| 85 | template<typename ImageTraits> |
| 86 | void create_region_with_differences_templ(const Image* a, |
| 87 | const Image* b, |
| 88 | const gfx::Rect& bounds, |
| 89 | gfx::Region& output) |
| 90 | { |
| 91 | for (int y=bounds.y; y<bounds.y2(); ++y) { |
| 92 | for (int x=bounds.x; x<bounds.x2(); ++x) { |
| 93 | if (get_pixel_fast<ImageTraits>(a, x, y) != |
| 94 | get_pixel_fast<ImageTraits>(b, x, y)) { |
| 95 | output.createUnion(output, gfx::Region(gfx::Rect(x, y, 1, 1))); |
| 96 | } |
| 97 | } |
| 98 | } |
| 99 | } |
| 100 | |
| 101 | // TODO merge this with Sprite::getTilemapsByTileset() |
| 102 | template<typename UnaryFunction> |
| 103 | void for_each_tile_using_tileset(Tileset* tileset, UnaryFunction f) |
| 104 | { |
| 105 | for (Cel* cel : tileset->sprite()->uniqueCels()) { |
| 106 | if (!cel->layer()->isTilemap() || |
| 107 | static_cast<LayerTilemap*>(cel->layer())->tileset() != tileset) |
| 108 | continue; |
| 109 | |
| 110 | Image* tilemapImage = cel->image(); |
| 111 | for_each_pixel<TilemapTraits>(tilemapImage, f); |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | struct Mod { |
| 116 | tile_index tileIndex; |
| 117 | ImageRef tileDstImage; |
| 118 | ImageRef tileImage; |
| 119 | gfx::Region tileRgn; |
| 120 | }; |
| 121 | |
| 122 | } // anonymous namespace |
| 123 | |
| 124 | void create_region_with_differences(const Image* a, |
| 125 | const Image* b, |
| 126 | const gfx::Rect& bounds, |
| 127 | gfx::Region& output) |
| 128 | { |
| 129 | ASSERT(a->pixelFormat() == b->pixelFormat()); |
| 130 | switch (a->pixelFormat()) { |
| 131 | case IMAGE_RGB: create_region_with_differences_templ<RgbTraits>(a, b, bounds, output); break; |
| 132 | case IMAGE_GRAYSCALE: create_region_with_differences_templ<GrayscaleTraits>(a, b, bounds, output); break; |
| 133 | case IMAGE_INDEXED: create_region_with_differences_templ<IndexedTraits>(a, b, bounds, output); break; |
| 134 | } |
| 135 | } |
| 136 | |
| 137 | static void remove_unused_tiles_from_tileset( |
| 138 | CmdSequence* cmds, |
| 139 | doc::Tileset* tileset, |
| 140 | std::vector<size_t>& tilesHistogram, |
| 141 | const std::vector<bool>& modifiedTileIndexes); |
| 142 | |
| 143 | doc::ImageRef crop_cel_image( |
| 144 | const doc::Cel* cel, |
| 145 | const color_t bgcolor) |
| 146 | { |
| 147 | doc::Sprite* sprite = cel->sprite(); |
| 148 | |
| 149 | if (cel->layer()->isTilemap()) { |
| 150 | doc::ImageRef dstImage(doc::Image::create(sprite->spec())); |
| 151 | |
| 152 | render::Render().renderCel( |
| 153 | dstImage.get(), |
| 154 | cel, |
| 155 | sprite, |
| 156 | cel->image(), |
| 157 | cel->layer(), |
| 158 | sprite->palette(cel->frame()), |
| 159 | dstImage->bounds(), |
| 160 | gfx::Clip(cel->position(), dstImage->bounds()), |
| 161 | 255, BlendMode::NORMAL); |
| 162 | |
| 163 | return dstImage; |
| 164 | } |
| 165 | else { |
| 166 | return doc::ImageRef( |
| 167 | doc::crop_image( |
| 168 | cel->image(), |
| 169 | gfx::Rect(sprite->bounds()).offset(-cel->position()), |
| 170 | bgcolor)); |
| 171 | } |
| 172 | } |
| 173 | |
| 174 | Cel* create_cel_copy(CmdSequence* cmds, |
| 175 | const Cel* srcCel, |
| 176 | const Sprite* dstSprite, |
| 177 | Layer* dstLayer, |
| 178 | const frame_t dstFrame) |
| 179 | { |
| 180 | const Image* srcImage = srcCel->image(); |
| 181 | doc::PixelFormat dstPixelFormat = |
| 182 | (dstLayer->isTilemap() ? IMAGE_TILEMAP: |
| 183 | dstSprite->pixelFormat()); |
| 184 | gfx::Size dstSize(srcImage->width(), |
| 185 | srcImage->height()); |
| 186 | |
| 187 | // From Tilemap -> Image |
| 188 | if (srcCel->layer()->isTilemap() && !dstLayer->isTilemap()) { |
| 189 | auto layerTilemap = static_cast<doc::LayerTilemap*>(srcCel->layer()); |
| 190 | dstSize = layerTilemap->tileset()->grid().tilemapSizeToCanvas(dstSize); |
| 191 | } |
| 192 | // From Image or Tilemap -> Tilemap |
| 193 | else if (dstLayer->isTilemap()) { |
| 194 | auto dstLayerTilemap = static_cast<doc::LayerTilemap*>(dstLayer); |
| 195 | |
| 196 | // Tilemap -> Tilemap |
| 197 | Grid grid; |
| 198 | if (srcCel->layer()->isTilemap()) { |
| 199 | grid = dstLayerTilemap->tileset()->grid(); |
| 200 | if (srcCel->layer()->isTilemap()) |
| 201 | grid.origin(srcCel->position()); |
| 202 | } |
| 203 | // Image -> Tilemap |
| 204 | else { |
| 205 | auto gridBounds = dstLayerTilemap->sprite()->gridBounds(); |
| 206 | grid.origin(gridBounds.origin()); |
| 207 | grid.tileSize(gridBounds.size()); |
| 208 | } |
| 209 | |
| 210 | const gfx::Rect tilemapBounds = grid.canvasToTile(srcCel->bounds()); |
| 211 | dstSize = tilemapBounds.size(); |
| 212 | } |
| 213 | |
| 214 | // New cel |
| 215 | std::unique_ptr<Cel> dstCel( |
| 216 | new Cel(dstFrame, ImageRef(Image::create(dstPixelFormat, dstSize.w, dstSize.h)))); |
| 217 | |
| 218 | dstCel->setOpacity(srcCel->opacity()); |
| 219 | dstCel->data()->setUserData(srcCel->data()->userData()); |
| 220 | |
| 221 | // Special case were we copy from a tilemap... |
| 222 | if (srcCel->layer()->isTilemap()) { |
| 223 | if (dstLayer->isTilemap()) { |
| 224 | // Tilemap -> Tilemap (with same tileset) |
| 225 | // Best case, copy a cel in the same layer (we have the same |
| 226 | // tileset available, so we just copy the tilemap as it is). |
| 227 | if (srcCel->layer() == dstLayer) { |
| 228 | dstCel->image()->copy(srcImage, gfx::Clip(0, 0, srcImage->bounds())); |
| 229 | } |
| 230 | // Tilemap -> Tilemap (with different tilesets) |
| 231 | else { |
| 232 | doc::ImageSpec spec = dstSprite->spec(); |
| 233 | spec.setSize(srcCel->bounds().size()); |
| 234 | doc::ImageRef tmpImage(doc::Image::create(spec)); |
| 235 | render::Render().renderCel( |
| 236 | tmpImage.get(), |
| 237 | srcCel, |
| 238 | dstSprite, |
| 239 | srcImage, |
| 240 | srcCel->layer(), |
| 241 | dstSprite->palette(dstCel->frame()), |
| 242 | gfx::Rect(gfx::Point(0, 0), srcCel->bounds().size()), |
| 243 | gfx::Clip(0, 0, tmpImage->bounds()), |
| 244 | 255, BlendMode::NORMAL); |
| 245 | |
| 246 | doc::ImageRef tilemap = dstCel->imageRef(); |
| 247 | |
| 248 | draw_image_into_new_tilemap_cel( |
| 249 | cmds, static_cast<doc::LayerTilemap*>(dstLayer), dstCel.get(), |
| 250 | tmpImage.get(), |
| 251 | srcCel->bounds().origin(), |
| 252 | srcCel->bounds().origin(), |
| 253 | srcCel->bounds(), |
| 254 | tilemap); |
| 255 | } |
| 256 | dstCel->setPosition(srcCel->position()); |
| 257 | } |
| 258 | // Tilemap -> Image (so we convert the tilemap to a regular image) |
| 259 | else { |
| 260 | render::Render().renderCel( |
| 261 | dstCel->image(), |
| 262 | srcCel, |
| 263 | dstSprite, |
| 264 | srcImage, |
| 265 | srcCel->layer(), |
| 266 | dstSprite->palette(dstCel->frame()), |
| 267 | gfx::Rect(gfx::Point(0, 0), srcCel->bounds().size()), |
| 268 | gfx::Clip(0, 0, dstCel->image()->bounds()), |
| 269 | 255, BlendMode::NORMAL); |
| 270 | |
| 271 | // Shrink image |
| 272 | if (dstLayer->isTransparent()) { |
| 273 | auto bg = dstCel->image()->maskColor(); |
| 274 | gfx::Rect bounds; |
| 275 | if (algorithm::shrink_bounds(dstCel->image(), bg, dstLayer, bounds)) { |
| 276 | ImageRef trimmed(doc::crop_image(dstCel->image(), bounds, bg)); |
| 277 | dstCel->data()->setImage(trimmed, dstLayer); |
| 278 | dstCel->setPosition(srcCel->position() + bounds.origin()); |
| 279 | return dstCel.release(); |
| 280 | } |
| 281 | } |
| 282 | } |
| 283 | } |
| 284 | // Image -> Tilemap (we'll need to generate new tilesets) |
| 285 | else if (dstLayer->isTilemap()) { |
| 286 | doc::ImageRef tilemap = dstCel->imageRef(); |
| 287 | draw_image_into_new_tilemap_cel( |
| 288 | cmds, static_cast<doc::LayerTilemap*>(dstLayer), dstCel.get(), |
| 289 | srcImage, |
| 290 | // Use the grid origin of the sprite |
| 291 | srcCel->sprite()->gridBounds().origin(), |
| 292 | srcCel->bounds().origin(), |
| 293 | srcCel->bounds(), |
| 294 | tilemap); |
| 295 | } |
| 296 | else if ((dstSprite->pixelFormat() != srcImage->pixelFormat()) || |
| 297 | // If both images are indexed but with different palette, we can |
| 298 | // convert the source cel to RGB first. |
| 299 | (dstSprite->pixelFormat() == IMAGE_INDEXED && |
| 300 | srcImage->pixelFormat() == IMAGE_INDEXED && |
| 301 | srcCel->sprite()->palette(srcCel->frame())->countDiff( |
| 302 | dstSprite->palette(dstFrame), nullptr, nullptr))) { |
| 303 | ImageRef tmpImage(Image::create(IMAGE_RGB, srcImage->width(), srcImage->height())); |
| 304 | tmpImage->clear(0); |
| 305 | |
| 306 | render::convert_pixel_format( |
| 307 | srcImage, |
| 308 | tmpImage.get(), |
| 309 | IMAGE_RGB, |
| 310 | render::Dithering(), |
| 311 | srcCel->sprite()->rgbMap(srcCel->frame()), |
| 312 | srcCel->sprite()->palette(srcCel->frame()), |
| 313 | srcCel->layer()->isBackground(), |
| 314 | 0); |
| 315 | |
| 316 | render::convert_pixel_format( |
| 317 | tmpImage.get(), |
| 318 | dstCel->image(), |
| 319 | IMAGE_INDEXED, |
| 320 | render::Dithering(), |
| 321 | dstSprite->rgbMap(dstFrame), |
| 322 | dstSprite->palette(dstFrame), |
| 323 | srcCel->layer()->isBackground(), |
| 324 | dstSprite->transparentColor()); |
| 325 | } |
| 326 | // Simple case, where we copy both images |
| 327 | else { |
| 328 | render::composite_image( |
| 329 | dstCel->image(), |
| 330 | srcImage, |
| 331 | srcCel->sprite()->palette(srcCel->frame()), |
| 332 | 0, 0, 255, BlendMode::SRC); |
| 333 | } |
| 334 | |
| 335 | // Resize a referece cel to a non-reference layer |
| 336 | if (srcCel->layer()->isReference() && !dstLayer->isReference()) { |
| 337 | gfx::RectF srcBounds = srcCel->boundsF(); |
| 338 | |
| 339 | std::unique_ptr<Cel> dstCel2( |
| 340 | new Cel(dstFrame, |
| 341 | ImageRef(Image::create(dstSprite->pixelFormat(), |
| 342 | std::ceil(srcBounds.w), |
| 343 | std::ceil(srcBounds.h))))); |
| 344 | algorithm::resize_image( |
| 345 | dstCel->image(), dstCel2->image(), |
| 346 | algorithm::RESIZE_METHOD_NEAREST_NEIGHBOR, |
| 347 | nullptr, nullptr, 0); |
| 348 | |
| 349 | dstCel.reset(dstCel2.release()); |
| 350 | dstCel->setPosition(gfx::Point(srcBounds.origin())); |
| 351 | } |
| 352 | // Copy original cel bounds |
| 353 | else if (!dstLayer->isTilemap()) { |
| 354 | if (srcCel->layer() && |
| 355 | srcCel->layer()->isReference()) { |
| 356 | dstCel->setBoundsF(srcCel->boundsF()); |
| 357 | } |
| 358 | else { |
| 359 | dstCel->setPosition(srcCel->position()); |
| 360 | } |
| 361 | } |
| 362 | |
| 363 | return dstCel.release(); |
| 364 | } |
| 365 | |
| 366 | void draw_image_into_new_tilemap_cel( |
| 367 | CmdSequence* cmds, |
| 368 | doc::LayerTilemap* dstLayer, |
| 369 | doc::Cel* dstCel, |
| 370 | const doc::Image* srcImage, |
| 371 | const gfx::Point& gridOrigin, |
| 372 | const gfx::Point& srcImagePos, |
| 373 | const gfx::Rect& canvasBounds, |
| 374 | doc::ImageRef& newTilemap) |
| 375 | { |
| 376 | ASSERT(dstLayer->isTilemap()); |
| 377 | |
| 378 | doc::Tileset* tileset = dstLayer->tileset(); |
| 379 | doc::Grid grid = tileset->grid(); |
| 380 | grid.origin(gridOrigin); |
| 381 | |
| 382 | gfx::Size tileSize = grid.tileSize(); |
| 383 | const gfx::Rect tilemapBounds = grid.canvasToTile(canvasBounds); |
| 384 | |
| 385 | if (!newTilemap) { |
| 386 | newTilemap.reset(doc::Image::create(IMAGE_TILEMAP, |
| 387 | tilemapBounds.w, |
| 388 | tilemapBounds.h)); |
| 389 | newTilemap->setMaskColor(doc::notile); |
| 390 | newTilemap->clear(doc::notile); |
| 391 | } |
| 392 | else { |
| 393 | ASSERT(tilemapBounds.w == newTilemap->width()); |
| 394 | ASSERT(tilemapBounds.h == newTilemap->height()); |
| 395 | } |
| 396 | |
| 397 | for (const gfx::Point& tilePt : grid.tilesInCanvasRegion(gfx::Region(canvasBounds))) { |
| 398 | const gfx::Point tilePtInCanvas = grid.tileToCanvas(tilePt); |
| 399 | doc::ImageRef tileImage( |
| 400 | doc::crop_image(srcImage, |
| 401 | tilePtInCanvas.x-srcImagePos.x, |
| 402 | tilePtInCanvas.y-srcImagePos.y, |
| 403 | tileSize.w, tileSize.h, |
| 404 | srcImage->maskColor())); |
| 405 | if (grid.hasMask()) |
| 406 | mask_image(tileImage.get(), grid.mask().get()); |
| 407 | |
| 408 | preprocess_transparent_pixels(tileImage.get()); |
| 409 | |
| 410 | doc::tile_index tileIndex; |
| 411 | if (!tileset->findTileIndex(tileImage, tileIndex)) { |
| 412 | auto addTile = new cmd::AddTile(tileset, tileImage); |
| 413 | |
| 414 | if (cmds) |
| 415 | cmds->executeAndAdd(addTile); |
| 416 | else { |
| 417 | // TODO a little hacky |
| 418 | addTile->execute( |
| 419 | static_cast<Doc*>(dstLayer->sprite()->document())->context()); |
| 420 | } |
| 421 | |
| 422 | tileIndex = addTile->tileIndex(); |
| 423 | |
| 424 | if (!cmds) |
| 425 | delete addTile; |
| 426 | } |
| 427 | |
| 428 | // We were using newTilemap->putPixel() directly but received a |
| 429 | // crash report about an "access violation". So now we've added |
| 430 | // some checks to the operation. |
| 431 | { |
| 432 | const int u = tilePt.x-tilemapBounds.x; |
| 433 | const int v = tilePt.y-tilemapBounds.y; |
| 434 | ASSERT((u >= 0) && (v >= 0) && (u < newTilemap->width()) && (v < newTilemap->height())); |
| 435 | doc::put_pixel(newTilemap.get(), u, v, tileIndex); |
| 436 | } |
| 437 | } |
| 438 | |
| 439 | static_cast<Doc*>(dstLayer->sprite()->document()) |
| 440 | ->notifyTilesetChanged(tileset); |
| 441 | |
| 442 | dstCel->data()->setImage(newTilemap, dstLayer); |
| 443 | dstCel->setPosition(grid.tileToCanvas(tilemapBounds.origin())); |
| 444 | } |
| 445 | |
| 446 | void modify_tilemap_cel_region( |
| 447 | CmdSequence* cmds, |
| 448 | doc::Cel* cel, |
| 449 | doc::Tileset* tileset, |
| 450 | const gfx::Region& region, |
| 451 | const TilesetMode tilesetMode, |
| 452 | const GetTileImageFunc& getTileImage, |
| 453 | const gfx::Region& forceRegion) |
| 454 | { |
| 455 | OPS_TRACE("modify_tilemap_cel_region %d %d %d %d\n" , |
| 456 | region.bounds().x, region.bounds().y, |
| 457 | region.bounds().w, region.bounds().h); |
| 458 | |
| 459 | if (region.isEmpty()) |
| 460 | return; |
| 461 | |
| 462 | ASSERT(cel->layer() && cel->layer()->isTilemap()); |
| 463 | ASSERT(cel->image()->pixelFormat() == IMAGE_TILEMAP); |
| 464 | |
| 465 | doc::LayerTilemap* tilemapLayer = static_cast<doc::LayerTilemap*>(cel->layer()); |
| 466 | |
| 467 | Doc* doc = static_cast<Doc*>(tilemapLayer->sprite()->document()); |
| 468 | bool addUndoToTileset = false; |
| 469 | if (!tileset) { |
| 470 | tileset = tilemapLayer->tileset(); |
| 471 | addUndoToTileset = true; |
| 472 | } |
| 473 | doc::Grid grid = tileset->grid(); |
| 474 | grid.origin(grid.origin() + cel->position()); |
| 475 | |
| 476 | const gfx::Size tileSize = grid.tileSize(); |
| 477 | const gfx::Rect oldTilemapBounds(grid.canvasToTile(cel->position()), |
| 478 | cel->image()->bounds().size()); |
| 479 | const gfx::Rect patchTilemapBounds = grid.canvasToTile(region.bounds()); |
| 480 | const gfx::Rect newTilemapBounds = (oldTilemapBounds | patchTilemapBounds); |
| 481 | |
| 482 | OPS_TRACE("modify_tilemap_cel_region:\n" |
| 483 | " - grid.origin =%d %d\n" |
| 484 | " - cel.position =%d %d\n" |
| 485 | " - oldTilemapBounds =%d %d %d %d\n" |
| 486 | " - patchTilemapBounds=%d %d %d %d (region.bounds = %d %d %d %d)\n" |
| 487 | " - newTilemapBounds =%d %d %d %d\n" , |
| 488 | grid.origin().x, grid.origin().y, |
| 489 | cel->position().x, cel->position().y, |
| 490 | oldTilemapBounds.x, oldTilemapBounds.y, oldTilemapBounds.w, oldTilemapBounds.h, |
| 491 | patchTilemapBounds.x, patchTilemapBounds.y, patchTilemapBounds.w, patchTilemapBounds.h, |
| 492 | region.bounds().x, region.bounds().y, region.bounds().w, region.bounds().h, |
| 493 | newTilemapBounds.x, newTilemapBounds.y, newTilemapBounds.w, newTilemapBounds.h); |
| 494 | |
| 495 | // Autogenerate tiles |
| 496 | if (tilesetMode == TilesetMode::Auto || |
| 497 | tilesetMode == TilesetMode::Stack) { |
| 498 | // TODO create a smaller image |
| 499 | doc::ImageRef newTilemap( |
| 500 | doc::Image::create(IMAGE_TILEMAP, |
| 501 | newTilemapBounds.w, |
| 502 | newTilemapBounds.h)); |
| 503 | |
| 504 | newTilemap->setMaskColor(doc::notile); |
| 505 | newTilemap->clear(doc::notile); // TODO find the tile with empty content? |
| 506 | newTilemap->copy( |
| 507 | cel->image(), |
| 508 | gfx::Clip(oldTilemapBounds.x-newTilemapBounds.x, |
| 509 | oldTilemapBounds.y-newTilemapBounds.y, 0, 0, |
| 510 | oldTilemapBounds.w, oldTilemapBounds.h)); |
| 511 | |
| 512 | gfx::Region tilePtsRgn; |
| 513 | |
| 514 | // This region includes the modified region by the user + the |
| 515 | // extra region added as we've incremented the tilemap size |
| 516 | // (newTilemapBounds). |
| 517 | gfx::Region regionToPatch(grid.tileToCanvas(newTilemapBounds)); |
| 518 | regionToPatch -= gfx::Region(grid.tileToCanvas(oldTilemapBounds)); |
| 519 | regionToPatch |= region; |
| 520 | |
| 521 | std::vector<bool> modifiedTileIndexes(tileset->size(), false); |
| 522 | std::vector<size_t> tilesHistogram(tileset->size(), 0); |
| 523 | if (tilesetMode == TilesetMode::Auto) { |
| 524 | for_each_tile_using_tileset( |
| 525 | tileset, [tileset, &tilesHistogram](const doc::tile_t t){ |
| 526 | if (t != doc::notile) { |
| 527 | doc::tile_index ti = doc::tile_geti(t); |
| 528 | if (ti >= 0 && ti < tileset->size()) |
| 529 | ++tilesHistogram[ti]; |
| 530 | } |
| 531 | }); |
| 532 | } |
| 533 | |
| 534 | for (const gfx::Point& tilePt : grid.tilesInCanvasRegion(regionToPatch)) { |
| 535 | const int u = tilePt.x-newTilemapBounds.x; |
| 536 | const int v = tilePt.y-newTilemapBounds.y; |
| 537 | OPS_TRACE(" - modify tile xy=%d %d uv=%d %d\n" , tilePt.x, tilePt.y, u, v); |
| 538 | if (!newTilemap->bounds().contains(u, v)) |
| 539 | continue; |
| 540 | |
| 541 | const doc::tile_t t = newTilemap->getPixel(u, v); |
| 542 | const doc::tile_index ti = (t != doc::notile ? doc::tile_geti(t): doc::notile); |
| 543 | const doc::ImageRef existentTileImage = tileset->get(ti); |
| 544 | |
| 545 | const gfx::Rect tileInCanvasRc(grid.tileToCanvas(tilePt), tileSize); |
| 546 | ImageRef tileImage(getTileImage(existentTileImage, tileInCanvasRc)); |
| 547 | if (grid.hasMask()) |
| 548 | mask_image(tileImage.get(), grid.mask().get()); |
| 549 | |
| 550 | preprocess_transparent_pixels(tileImage.get()); |
| 551 | |
| 552 | tile_index tileIndex; |
| 553 | if (tileset->findTileIndex(tileImage, tileIndex)) { |
| 554 | // We can re-use an existent tile (tileIndex) from the tileset |
| 555 | } |
| 556 | else if (tilesetMode == TilesetMode::Auto && |
| 557 | t != doc::notile && |
| 558 | ti >= 0 && ti < tilesHistogram.size() && |
| 559 | // If the tile is just used once, we can modify this |
| 560 | // same tile |
| 561 | tilesHistogram[ti] == 1) { |
| 562 | // Common case: Re-utilize the same tile in Auto mode. |
| 563 | tileIndex = ti; |
| 564 | cmds->executeAndAdd( |
| 565 | new cmd::CopyTileRegion( |
| 566 | existentTileImage.get(), |
| 567 | tileImage.get(), |
| 568 | gfx::Region(tileImage->bounds()), // TODO calculate better region |
| 569 | gfx::Point(0, 0), |
| 570 | false, |
| 571 | tileIndex, |
| 572 | tileset)); |
| 573 | } |
| 574 | else { |
| 575 | auto addTile = new cmd::AddTile(tileset, tileImage); |
| 576 | cmds->executeAndAdd(addTile); |
| 577 | |
| 578 | tileIndex = addTile->tileIndex(); |
| 579 | } |
| 580 | |
| 581 | // If the tile changed, we have to remove the old tile index |
| 582 | // (ti) from the histogram count. |
| 583 | if (tilesetMode == TilesetMode::Auto && |
| 584 | t != doc::notile && |
| 585 | ti >= 0 && ti < tilesHistogram.size() && |
| 586 | ti != tileIndex) { |
| 587 | --tilesHistogram[ti]; |
| 588 | |
| 589 | // It indicates that the tile "ti" was modified to |
| 590 | // "tileIndex", so then, in case that we have to remove tiles, |
| 591 | // we can check the ones that were modified & are unused. |
| 592 | modifiedTileIndexes[ti] = true; |
| 593 | } |
| 594 | |
| 595 | OPS_TRACE(" - tile %d -> %d\n" , |
| 596 | (t == doc::notile ? -1: ti), |
| 597 | tileIndex); |
| 598 | |
| 599 | const doc::tile_t tile = doc::tile(tileIndex, 0); |
| 600 | if (t != tile) { |
| 601 | newTilemap->putPixel(u, v, tile); |
| 602 | tilePtsRgn |= gfx::Region(gfx::Rect(u, v, 1, 1)); |
| 603 | |
| 604 | // We add the new one tileIndex in the histogram count. |
| 605 | if (tilesetMode == TilesetMode::Auto && |
| 606 | tileIndex != doc::notile && |
| 607 | tileIndex >= 0 && tileIndex < tilesHistogram.size()) { |
| 608 | ++tilesHistogram[tileIndex]; |
| 609 | } |
| 610 | } |
| 611 | } |
| 612 | |
| 613 | if (newTilemap->width() != cel->image()->width() || |
| 614 | newTilemap->height() != cel->image()->height()) { |
| 615 | gfx::Point newPos = grid.tileToCanvas(newTilemapBounds.origin()); |
| 616 | if (cel->position() != newPos) { |
| 617 | cmds->executeAndAdd( |
| 618 | new cmd::SetCelPosition(cel, newPos.x, newPos.y)); |
| 619 | } |
| 620 | cmds->executeAndAdd( |
| 621 | new cmd::ReplaceImage(cel->sprite(), cel->imageRef(), newTilemap)); |
| 622 | } |
| 623 | else if (!tilePtsRgn.isEmpty()) { |
| 624 | cmds->executeAndAdd( |
| 625 | new cmd::CopyRegion( |
| 626 | cel->image(), |
| 627 | newTilemap.get(), |
| 628 | tilePtsRgn, |
| 629 | gfx::Point(0, 0))); |
| 630 | } |
| 631 | |
| 632 | // Remove unused tiles |
| 633 | if (tilesetMode == TilesetMode::Auto) { |
| 634 | remove_unused_tiles_from_tileset(cmds, tileset, |
| 635 | tilesHistogram, |
| 636 | modifiedTileIndexes); |
| 637 | } |
| 638 | |
| 639 | doc->notifyTilesetChanged(tileset); |
| 640 | } |
| 641 | // Modify active set of tiles manually / don't auto-generate new tiles |
| 642 | else if (tilesetMode == TilesetMode::Manual) { |
| 643 | std::vector<Mod> mods; |
| 644 | |
| 645 | for (const gfx::Point& tilePt : grid.tilesInCanvasRegion(region)) { |
| 646 | // Ignore modifications outside the tilemap |
| 647 | if (!cel->image()->bounds().contains(tilePt.x, tilePt.y)) |
| 648 | continue; |
| 649 | |
| 650 | const doc::tile_t t = cel->image()->getPixel(tilePt.x, tilePt.y); |
| 651 | if (t == doc::notile) |
| 652 | continue; |
| 653 | |
| 654 | const doc::tile_index ti = doc::tile_geti(t); |
| 655 | const doc::ImageRef existentTileImage = tileset->get(ti); |
| 656 | if (!existentTileImage) { |
| 657 | // TODO add support to fill the tileset with the tile "ti" |
| 658 | continue; |
| 659 | } |
| 660 | |
| 661 | const gfx::Rect tileInCanvasRc(grid.tileToCanvas(tilePt), tileSize); |
| 662 | ImageRef tileImage(getTileImage(existentTileImage, tileInCanvasRc)); |
| 663 | if (grid.hasMask()) |
| 664 | mask_image(tileImage.get(), grid.mask().get()); |
| 665 | |
| 666 | gfx::Region tileRgn(tileInCanvasRc); |
| 667 | tileRgn.createIntersection(tileRgn, region); |
| 668 | tileRgn.offset(-tileInCanvasRc.origin()); |
| 669 | |
| 670 | ImageRef tileDstImage = tileset->get(ti); |
| 671 | |
| 672 | // Compare with the original tile from the original tileset |
| 673 | gfx::Region diffRgn; |
| 674 | create_region_with_differences(tilemapLayer->tileset()->get(ti).get(), |
| 675 | tileImage.get(), |
| 676 | tileRgn.bounds(), |
| 677 | diffRgn); |
| 678 | |
| 679 | // Keep only the modified region for this specific modification |
| 680 | tileRgn &= diffRgn; |
| 681 | |
| 682 | if (!forceRegion.isEmpty()) { |
| 683 | gfx::Region fr(forceRegion); |
| 684 | fr.offset(-tileInCanvasRc.origin()); |
| 685 | tileRgn |= fr; |
| 686 | } |
| 687 | |
| 688 | if (!tileRgn.isEmpty()) { |
| 689 | if (addUndoToTileset) { |
| 690 | Mod mod; |
| 691 | mod.tileIndex = ti; |
| 692 | mod.tileDstImage = tileDstImage; |
| 693 | mod.tileImage = tileImage; |
| 694 | mod.tileRgn = tileRgn; |
| 695 | mods.push_back(mod); |
| 696 | } |
| 697 | else { |
| 698 | copy_image(tileDstImage.get(), |
| 699 | tileImage.get(), |
| 700 | tileRgn); |
| 701 | tileset->notifyTileContentChange(ti); |
| 702 | } |
| 703 | } |
| 704 | } |
| 705 | |
| 706 | // Apply all modifications to tiles |
| 707 | if (addUndoToTileset) { |
| 708 | for (auto& mod : mods) { |
| 709 | // TODO avoid creating several CopyTileRegion for the same tile, |
| 710 | // merge all mods for the same tile in some way |
| 711 | cmds->executeAndAdd( |
| 712 | new cmd::CopyTileRegion( |
| 713 | mod.tileDstImage.get(), |
| 714 | mod.tileImage.get(), |
| 715 | mod.tileRgn, |
| 716 | gfx::Point(0, 0), |
| 717 | false, |
| 718 | mod.tileIndex, |
| 719 | tileset)); |
| 720 | } |
| 721 | } |
| 722 | |
| 723 | doc->notifyTilesetChanged(tileset); |
| 724 | } |
| 725 | |
| 726 | #ifdef _DEBUG |
| 727 | tileset->assertValidHashTable(); |
| 728 | #endif |
| 729 | } |
| 730 | |
| 731 | void clear_mask_from_cel(CmdSequence* cmds, |
| 732 | doc::Cel* cel, |
| 733 | const TilemapMode tilemapMode, |
| 734 | const TilesetMode tilesetMode) |
| 735 | { |
| 736 | ASSERT(cmds); |
| 737 | ASSERT(cel); |
| 738 | ASSERT(cel->layer()); |
| 739 | |
| 740 | if (cel->layer()->isTilemap() && tilemapMode == TilemapMode::Pixels) { |
| 741 | Doc* doc = static_cast<Doc*>(cel->document()); |
| 742 | |
| 743 | // Simple case (there is no visible selection, so we remove the |
| 744 | // whole cel) |
| 745 | if (!doc->isMaskVisible()) { |
| 746 | cmds->executeAndAdd(new cmd::ClearCel(cel)); |
| 747 | return; |
| 748 | } |
| 749 | |
| 750 | color_t bgcolor = doc->bgColor(cel->layer()); |
| 751 | doc::Mask* mask = doc->mask(); |
| 752 | |
| 753 | modify_tilemap_cel_region( |
| 754 | cmds, cel, nullptr, |
| 755 | gfx::Region(doc->mask()->bounds()), |
| 756 | tilesetMode, |
| 757 | [bgcolor, mask](const doc::ImageRef& origTile, |
| 758 | const gfx::Rect& tileBoundsInCanvas) -> doc::ImageRef { |
| 759 | doc::ImageRef modified(doc::Image::createCopy(origTile.get())); |
| 760 | doc::algorithm::fill_selection( |
| 761 | modified.get(), |
| 762 | tileBoundsInCanvas, |
| 763 | mask, |
| 764 | bgcolor, |
| 765 | nullptr); |
| 766 | return modified; |
| 767 | }); |
| 768 | } |
| 769 | else { |
| 770 | cmds->executeAndAdd(new cmd::ClearMask(cel)); |
| 771 | } |
| 772 | } |
| 773 | |
| 774 | static void remove_unused_tiles_from_tileset( |
| 775 | CmdSequence* cmds, |
| 776 | doc::Tileset* tileset, |
| 777 | std::vector<size_t>& tilesHistogram, |
| 778 | const std::vector<bool>& modifiedTileIndexes) |
| 779 | { |
| 780 | OPS_TRACE("remove_unused_tiles_from_tileset\n" ); |
| 781 | |
| 782 | int n = tileset->size(); |
| 783 | #ifdef _DEBUG |
| 784 | // Histogram just to check that we've a correct tilesHistogram |
| 785 | std::vector<size_t> tilesHistogram2(n, 0); |
| 786 | #endif |
| 787 | |
| 788 | for_each_tile_using_tileset( |
| 789 | tileset, |
| 790 | [&n |
| 791 | #ifdef _DEBUG |
| 792 | , &tilesHistogram2 |
| 793 | #endif |
| 794 | ](const doc::tile_t t){ |
| 795 | if (t != doc::notile) { |
| 796 | const doc::tile_index ti = doc::tile_geti(t); |
| 797 | n = std::max<int>(n, ti+1); |
| 798 | #ifdef _DEBUG |
| 799 | // This check is necessary in case the tilemap has a reference |
| 800 | // to a tile outside the valid range (e.g. when we resize the |
| 801 | // tileset deleting tiles that will not be present anymore) |
| 802 | if (ti >= 0 && ti < tilesHistogram2.size()) |
| 803 | ++tilesHistogram2[ti]; |
| 804 | #endif |
| 805 | } |
| 806 | }); |
| 807 | |
| 808 | #ifdef _DEBUG |
| 809 | for (int k=0; k<tilesHistogram.size(); ++k) { |
| 810 | OPS_TRACE("comparing [%d] -> %d vs %d\n" , k, tilesHistogram[k], tilesHistogram2[k]); |
| 811 | ASSERT(tilesHistogram[k] == tilesHistogram2[k]); |
| 812 | } |
| 813 | #endif |
| 814 | |
| 815 | doc::Remap remap(n); |
| 816 | doc::tile_index ti, tj; |
| 817 | ti = tj = 0; |
| 818 | for (; ti<remap.size(); ++ti) { |
| 819 | OPS_TRACE(" - ti=%d tj=%d tilesHistogram[%d]=%d\n" , |
| 820 | ti, tj, ti, (ti < tilesHistogram.size() ? tilesHistogram[ti]: 0)); |
| 821 | if (ti < tilesHistogram.size() && |
| 822 | tilesHistogram[ti] == 0 && |
| 823 | modifiedTileIndexes[ti]) { |
| 824 | cmds->executeAndAdd(new cmd::RemoveTile(tileset, tj)); |
| 825 | // Map to nothing, so the map can be invertible |
| 826 | remap.notile(ti); |
| 827 | } |
| 828 | else { |
| 829 | remap.map(ti, tj++); |
| 830 | } |
| 831 | } |
| 832 | |
| 833 | if (!remap.isIdentity()) { |
| 834 | #ifdef _DEBUG |
| 835 | for (ti=0; ti<remap.size(); ++ti) { |
| 836 | OPS_TRACE(" - remap tile[%d] -> %d\n" , ti, remap[ti]); |
| 837 | } |
| 838 | #endif |
| 839 | cmds->executeAndAdd(new cmd::RemapTilemaps(tileset, remap)); |
| 840 | } |
| 841 | } |
| 842 | |
| 843 | void move_tiles_in_tileset( |
| 844 | CmdSequence* cmds, |
| 845 | doc::Tileset* tileset, |
| 846 | doc::PalettePicks& picks, |
| 847 | int& currentEntry, |
| 848 | int beforeIndex) |
| 849 | { |
| 850 | OPS_TRACE("move_tiles_in_tileset\n" ); |
| 851 | |
| 852 | // We cannot move the empty tile (index 0) no any place |
| 853 | if (beforeIndex == 0) |
| 854 | ++beforeIndex; |
| 855 | if (picks.size() > 0 && picks[0]) |
| 856 | picks[0] = false; |
| 857 | if (!picks.picks()) |
| 858 | return; |
| 859 | |
| 860 | picks.resize(std::max<int>(picks.size(), beforeIndex)); |
| 861 | |
| 862 | int n = beforeIndex - tileset->size(); |
| 863 | if (n > 0) { |
| 864 | while (n-- > 0) |
| 865 | cmds->executeAndAdd(new cmd::AddTile(tileset, tileset->makeEmptyTile())); |
| 866 | } |
| 867 | |
| 868 | Remap remap = create_remap_to_move_picks(picks, beforeIndex); |
| 869 | cmds->executeAndAdd(new cmd::RemapTileset(tileset, remap)); |
| 870 | |
| 871 | // New selection |
| 872 | auto oldPicks = picks; |
| 873 | for (int i=0; i<picks.size(); ++i) |
| 874 | picks[remap[i]] = oldPicks[i]; |
| 875 | currentEntry = remap[currentEntry]; |
| 876 | } |
| 877 | |
| 878 | void copy_tiles_in_tileset( |
| 879 | CmdSequence* cmds, |
| 880 | doc::Tileset* tileset, |
| 881 | doc::PalettePicks& picks, |
| 882 | int& currentEntry, |
| 883 | int beforeIndex) |
| 884 | { |
| 885 | // We cannot move tiles before the empty tile |
| 886 | if (beforeIndex == 0) |
| 887 | ++beforeIndex; |
| 888 | |
| 889 | OPS_TRACE("copy_tiles_in_tileset beforeIndex=%d npicks=%d\n" , beforeIndex, picks.picks()); |
| 890 | |
| 891 | std::vector<ImageRef> newTiles; |
| 892 | for (int i=0; i<picks.size(); ++i) { |
| 893 | if (!picks[i]) |
| 894 | continue; |
| 895 | else if (i >= 0 && i < tileset->size()) { |
| 896 | newTiles.emplace_back(Image::createCopy(tileset->get(i).get())); |
| 897 | } |
| 898 | else { |
| 899 | newTiles.emplace_back(tileset->makeEmptyTile()); |
| 900 | } |
| 901 | } |
| 902 | |
| 903 | int n; |
| 904 | if (beforeIndex >= picks.size()) { |
| 905 | n = beforeIndex; |
| 906 | picks.resize(n); |
| 907 | } |
| 908 | else { |
| 909 | n = tileset->size(); |
| 910 | } |
| 911 | |
| 912 | const int npicks = picks.picks(); |
| 913 | const int m = n + npicks; |
| 914 | int j = 0; |
| 915 | picks.resize(m); |
| 916 | ASSERT(newTiles.size() == npicks); |
| 917 | for (int i=0; i<m; ++i) { |
| 918 | picks[i] = (i >= beforeIndex && i < beforeIndex + npicks); |
| 919 | if (picks[i]) { |
| 920 | // Fill the gap between the end of the tileset and the |
| 921 | // "beforeIndex" with empty tiles |
| 922 | while (tileset->size() < i) |
| 923 | cmds->executeAndAdd(new cmd::AddTile(tileset, tileset->makeEmptyTile())); |
| 924 | |
| 925 | tileset->insert(i, newTiles[j++]); |
| 926 | cmds->executeAndAdd(new cmd::AddTile(tileset, i)); |
| 927 | } |
| 928 | } |
| 929 | } |
| 930 | |
| 931 | } // namespace app |
| 932 | |