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
52namespace app {
53
54using namespace doc;
55
56namespace {
57
58template<typename ImageTraits>
59void 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
75void 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
85template<typename ImageTraits>
86void 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()
102template<typename UnaryFunction>
103void 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
115struct Mod {
116 tile_index tileIndex;
117 ImageRef tileDstImage;
118 ImageRef tileImage;
119 gfx::Region tileRgn;
120};
121
122} // anonymous namespace
123
124void 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
137static 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
143doc::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
174Cel* 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
366void 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
446void 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
731void 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
774static 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
843void 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
878void 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