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 | |