1// Aseprite
2// Copyright (C) 2019-2020 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#define EXP_TRACE(...) // TRACEARGS(__VA_ARGS__)
9
10#ifdef HAVE_CONFIG_H
11#include "config.h"
12#endif
13
14#include "app/util/expand_cel_canvas.h"
15
16#include "app/app.h"
17#include "app/cmd/add_cel.h"
18#include "app/cmd/copy_rect.h"
19#include "app/cmd/copy_region.h"
20#include "app/cmd/patch_cel.h"
21#include "app/cmd_sequence.h"
22#include "app/context.h"
23#include "app/doc.h"
24#include "app/site.h"
25#include "app/util/cel_ops.h"
26#include "app/util/range_utils.h"
27#include "base/debug.h"
28#include "doc/algorithm/shrink_bounds.h"
29#include "doc/cel.h"
30#include "doc/image.h"
31#include "doc/image_impl.h"
32#include "doc/layer.h"
33#include "doc/layer_tilemap.h"
34#include "doc/primitives.h"
35#include "doc/sprite.h"
36#include "doc/tileset.h"
37#include "doc/tileset_hash_table.h"
38#include "doc/tilesets.h"
39#include "gfx/point_io.h"
40#include "gfx/rect_io.h"
41#include "gfx/size_io.h"
42#include "render/render.h"
43
44namespace {
45
46// We cannot have two ExpandCelCanvas instances at the same time
47// (because we share ImageBuffers between them).
48static app::ExpandCelCanvas* singleton = nullptr;
49
50static doc::ImageBufferPtr src_buffer;
51static doc::ImageBufferPtr dst_buffer;
52
53static void destroy_buffers()
54{
55 src_buffer.reset();
56 dst_buffer.reset();
57}
58
59static void create_buffers()
60{
61 if (!src_buffer) {
62 app::App::instance()->Exit.connect(&destroy_buffers);
63
64 src_buffer.reset(new doc::ImageBuffer(1));
65 dst_buffer.reset(new doc::ImageBuffer(1));
66 }
67}
68
69}
70
71namespace app {
72
73ExpandCelCanvas::ExpandCelCanvas(
74 Site site,
75 Layer* layer,
76 const TiledMode tiledMode,
77 CmdSequence* cmds,
78 const Flags flags)
79 : m_document(site.document())
80 , m_sprite(site.sprite())
81 , m_layer(layer)
82 , m_frame(site.frame())
83 , m_cel(nullptr)
84 , m_celImage(nullptr)
85 , m_celCreated(false)
86 , m_flags(flags)
87 , m_srcImage(nullptr)
88 , m_dstImage(nullptr)
89 , m_dstTileset(nullptr)
90 , m_closed(false)
91 , m_committed(false)
92 , m_cmds(cmds)
93 , m_grid(site.grid())
94 , m_tilemapMode(site.tilemapMode())
95 , m_tilesetMode(site.tilesetMode())
96{
97 if (m_layer && m_layer->isTilemap()) {
98 m_flags = Flags(m_flags | NeedsSource);
99 }
100 m_canCompareSrcVsDst = ((m_flags & NeedsSource) == NeedsSource);
101
102 ASSERT(!singleton);
103 singleton = this;
104
105 create_buffers();
106
107 if (previewSpecificLayerChanges()) {
108 m_cel = m_layer->cel(site.frame());
109 if (m_cel)
110 m_celImage = m_cel->imageRef();
111 }
112
113 // Create a new cel
114 if (!m_cel) {
115 m_celCreated = true;
116 m_cel = new Cel(site.frame(), ImageRef(NULL));
117 }
118
119 m_origCelPos = m_cel->position();
120
121 // Region to draw
122 gfx::Rect celBounds = (m_celCreated ? m_sprite->bounds():
123 m_cel->bounds());
124
125 gfx::Rect spriteBounds(0, 0,
126 m_sprite->width(),
127 m_sprite->height());
128
129 if (tiledMode == TiledMode::NONE) { // Non-tiled
130 m_bounds = celBounds.createUnion(spriteBounds);
131 }
132 else { // Tiled
133 m_bounds = spriteBounds;
134 }
135
136 if ((m_tilemapMode == TilemapMode::Tiles) ||
137 ((m_flags & PixelsBounds) == PixelsBounds)) {
138 // Bounds of the canvas in tiles.
139 m_bounds = m_grid.canvasToTile(m_bounds);
140
141 // New tiles bounds in pixels coordinates.
142 gfx::Rect newBoundsFromTiles = m_grid.tileToCanvas(m_bounds);
143
144 // As the grid origin depends on the current cel position (see
145 // Site::grid()), and we're going to modify the m_cel position
146 // temporarily, we need to adjust the grid to the new temporal
147 // grid origin matching the new m_dstImage position.
148 m_grid.origin(newBoundsFromTiles.origin());
149
150 // The origin of m_bounds must be in canvas position
151 if ((m_flags & PixelsBounds) == PixelsBounds) {
152 m_bounds = newBoundsFromTiles;
153 }
154 else {
155 // Bounds for the new cel which is a tilemap
156 m_bounds.setOrigin(newBoundsFromTiles.origin());
157 }
158 }
159
160 // We have to adjust the cel position to match the m_dstImage
161 // position (the new m_dstImage will be used in RenderEngine to
162 // draw this cel).
163 if (!isTilesetPreview())
164 m_cel->setPosition(m_bounds.origin());
165
166 EXP_TRACE("ExpandCelCanvas",
167 "m_cel->bounds()=", m_cel->bounds(),
168 "m_bounds=", m_bounds,
169 "m_grid=", m_grid.origin(), m_grid.tileSize());
170
171 if (m_celCreated) {
172 // Calling "getDestCanvas()" we create the m_dstImage
173 getDestCanvas();
174
175 m_cel->data()->setImage(m_dstImage, m_layer);
176
177 if (previewSpecificLayerChanges())
178 static_cast<LayerImage*>(m_layer)->addCel(m_cel);
179 }
180 else if (m_layer->isTilemap() &&
181 m_tilemapMode == TilemapMode::Tiles) {
182 getDestCanvas();
183 m_cel->data()->setImage(m_dstImage, m_layer);
184 }
185 // If we are in a tilemap, we use m_dstImage to draw pixels (instead
186 // of the tilemap image).
187 else if (m_layer->isTilemap() &&
188 m_tilemapMode == TilemapMode::Pixels &&
189 !isTilesetPreview()) {
190 getDestCanvas();
191 m_cel->data()->setImage(m_dstImage, m_layer);
192 }
193 else if (isTilesetPreview()) {
194 getDestTileset();
195 }
196}
197
198ExpandCelCanvas::~ExpandCelCanvas()
199{
200 ASSERT(singleton == this);
201 singleton = nullptr;
202
203 try {
204 if (!m_committed && !m_closed)
205 rollback();
206 }
207 catch (...) {
208 // Do nothing
209 }
210}
211
212void ExpandCelCanvas::commit()
213{
214 EXP_TRACE("ExpandCelCanvas::commit",
215 "validSrcRegion", m_validSrcRegion.bounds(),
216 "validDstRegion", m_validDstRegion.bounds());
217
218 ASSERT(!m_closed);
219 ASSERT(!m_committed);
220
221 if (!m_layer) {
222 m_committed = true;
223 return;
224 }
225
226 // Was the cel created in the start of the tool-loop?
227 if (m_celCreated) {
228 ASSERT(m_cel);
229 ASSERT(!m_celImage);
230
231 // Validate the whole m_dstImage (invalid areas are cleared, as we
232 // don't have a m_celImage)
233 validateDestCanvas(gfx::Region(m_bounds));
234
235 if (previewSpecificLayerChanges()) {
236 // We can temporary remove the cel.
237 static_cast<LayerImage*>(m_layer)->removeCel(m_cel);
238
239 gfx::Rect trimBounds = getTrimDstImageBounds();
240 if (!trimBounds.isEmpty()) {
241 // Convert the image to tiles
242 if (m_layer->isTilemap() &&
243 m_tilemapMode == TilemapMode::Pixels) {
244 doc::ImageRef newTilemap;
245 draw_image_into_new_tilemap_cel(
246 m_cmds, static_cast<doc::LayerTilemap*>(m_layer), m_cel,
247 // Draw the dst image in the tilemap
248 m_dstImage.get(),
249 m_origCelPos,
250 gfx::Point(0, 0), // m_dstImage->bounds(),
251 trimBounds,
252 newTilemap);
253 }
254 else {
255 ImageRef newImage(trimDstImage(trimBounds));
256 ASSERT(newImage);
257
258 m_cel->data()->setImage(newImage, m_layer);
259 m_cel->setPosition(
260 m_cel->position() +
261 (m_layer->isTilemap() ?
262 // TODO we should get the exact coordinate from getTrimDstImageBounds()
263 m_grid.tileToCanvas(trimBounds.origin()):
264 trimBounds.origin()));
265 }
266
267 // And add the cel again in the layer.
268 m_cmds->executeAndAdd(new cmd::AddCel(m_layer, m_cel));
269 }
270 else {
271 // Delete unused cel
272 delete m_cel;
273 m_cel = nullptr;
274 }
275 }
276 // We are selecting...
277 else {
278 ASSERT(isSelectionPreview());
279
280 // Just delete the created cel for preview purposes of the selection
281 delete m_cel;
282 m_cel = nullptr;
283 }
284 }
285 else if (m_celImage) {
286 // Restore cel position to its original position
287 m_cel->setPosition(m_origCelPos);
288
289#ifdef _DEBUG
290 if (m_layer->isTilemap() && !isTilesetPreview()) {
291 ASSERT(m_cel->image() != m_celImage.get());
292 }
293 else {
294 ASSERT(m_cel->image() == m_celImage.get());
295 }
296#endif
297
298 gfx::Region* regionToPatch = &m_validDstRegion;
299 gfx::Region reduced;
300
301 if (m_canCompareSrcVsDst) {
302 ASSERT(gfx::Region().createSubtraction(m_validDstRegion, m_validSrcRegion).isEmpty());
303
304 for (gfx::Rect rc : m_validDstRegion) {
305 if (algorithm::shrink_bounds2(getSourceCanvas(),
306 getDestCanvas(), rc, rc)) {
307 reduced |= gfx::Region(rc);
308 }
309 }
310
311 regionToPatch = &reduced;
312 }
313
314 EXP_TRACE(" - regionToPatch", regionToPatch->bounds());
315
316 // Convert the image to tiles again
317 if (m_layer->isTilemap() &&
318 m_tilemapMode == TilemapMode::Pixels) {
319 ASSERT(m_celImage->pixelFormat() == IMAGE_TILEMAP);
320
321 // Validate the whole m_dstImage (invalid areas are cleared, as we
322 // don't have a m_celImage)
323 validateDestCanvas(gfx::Region(m_bounds));
324
325 // Restore the original m_celImage, because the cel contained
326 // the m_dstImage temporally for drawing purposes. No undo
327 // information is required at this moment.
328 if (!m_dstTileset) {
329 ASSERT(m_celImage.get() != m_cel->image());
330 m_cel->data()->setImage(m_celImage, m_layer);
331
332 // Put the region in absolute sprite canvas coordinates (instead
333 // of relative to the m_cel).
334 regionToPatch->offset(m_bounds.origin());
335
336 modify_tilemap_cel_region(
337 m_cmds, m_cel, nullptr,
338 *regionToPatch,
339 m_tilesetMode,
340 [this](const doc::ImageRef& origTile,
341 const gfx::Rect& tileBoundsInCanvas) -> doc::ImageRef {
342 return trimDstImage(tileBoundsInCanvas);
343 });
344 }
345 else {
346 ASSERT(m_celImage.get() == m_cel->image());
347
348 const Tileset* srcTileset = static_cast<LayerTilemap*>(m_layer)->tileset();
349 ASSERT(srcTileset);
350 ASSERT(srcTileset->size() == m_dstTileset->size());
351
352 // Patch tiles
353 for (tile_index ti=1; ti<srcTileset->size(); ++ti) {
354 gfx::Region diffRgn;
355 create_region_with_differences(srcTileset->get(ti).get(),
356 m_dstTileset->get(ti).get(),
357 m_dstTileset->get(ti)->bounds(),
358 diffRgn);
359 if (!diffRgn.isEmpty()) {
360 m_cmds->executeAndAdd(
361 new cmd::CopyTileRegion(
362 srcTileset->get(ti).get(),
363 m_dstTileset->get(ti).get(),
364 diffRgn,
365 gfx::Point(0, 0),
366 false,
367 ti,
368 srcTileset));
369 }
370 }
371 }
372 }
373 // Check that the region to copy or patch is not empty before we
374 // create the new cmd
375 else if (!regionToPatch->isEmpty()) {
376 if (m_layer->isBackground()) {
377 // TODO support for tilemap backgrounds?
378 ASSERT(m_celImage.get() == m_cel->image());
379
380 m_cmds->executeAndAdd(
381 new cmd::CopyRegion(
382 m_cel->image(),
383 m_dstImage.get(),
384 *regionToPatch,
385 m_bounds.origin()));
386 }
387 else if (m_tilemapMode == TilemapMode::Tiles) {
388 ASSERT(m_celImage.get() != m_cel->image());
389
390 m_cel->data()->setImage(m_celImage, m_layer);
391 gfx::Region regionInCanvas = m_grid.tileToCanvas(*regionToPatch);
392
393 EXP_TRACE(" - Tilemap bounds to patch", regionInCanvas.bounds());
394
395 m_cmds->executeAndAdd(
396 new cmd::PatchCel(
397 m_cel,
398 m_dstImage.get(),
399 regionInCanvas,
400 m_grid.origin()));
401 }
402 else {
403 ASSERT(m_celImage.get() == m_cel->image());
404
405 m_cmds->executeAndAdd(
406 new cmd::PatchCel(
407 m_cel,
408 m_dstImage.get(),
409 *regionToPatch,
410 m_bounds.origin()));
411 }
412 }
413 // Restore the original cel image if needed (e.g. no region to
414 // patch on a tilemap)
415 else if (m_celImage.get() != m_cel->image()) {
416 m_cel->data()->setImage(m_celImage, m_layer);
417 }
418 }
419 else {
420 ASSERT(false);
421 }
422
423 m_committed = true;
424}
425
426void ExpandCelCanvas::rollback()
427{
428 ASSERT(!m_closed);
429 ASSERT(!m_committed);
430
431 // Here we destroy the temporary 'cel' created and restore all as it was before
432 m_cel->setPosition(m_origCelPos);
433
434 if (m_celCreated) {
435 if (previewSpecificLayerChanges())
436 static_cast<LayerImage*>(m_layer)->removeCel(m_cel);
437
438 delete m_cel;
439 m_celImage.reset();
440 }
441 // Restore the original tilemap
442 else if (m_layer->isTilemap()) {
443 ASSERT(m_celImage->pixelFormat() == IMAGE_TILEMAP);
444 m_cel->data()->setImage(m_celImage,
445 m_cel->layer());
446 }
447
448 m_closed = true;
449}
450
451gfx::Point ExpandCelCanvas::getCelOrigin() const
452{
453 if (isTilesetPreview())
454 return m_bounds.origin();
455 else
456 return m_cel->position();
457}
458
459Image* ExpandCelCanvas::getSourceCanvas()
460{
461 ASSERT((m_flags & NeedsSource) == NeedsSource);
462
463 if (!m_srcImage) {
464 if (m_tilemapMode == TilemapMode::Tiles) {
465 m_srcImage.reset(Image::create(IMAGE_TILEMAP,
466 m_bounds.w, m_bounds.h, src_buffer));
467 m_srcImage->setMaskColor(doc::notile);
468 }
469 else {
470 m_srcImage.reset(Image::create(m_sprite->pixelFormat(),
471 m_bounds.w, m_bounds.h, src_buffer));
472 m_srcImage->setMaskColor(m_sprite->transparentColor());
473 }
474 m_srcImage->clear(m_srcImage->maskColor());
475 }
476 return m_srcImage.get();
477}
478
479Image* ExpandCelCanvas::getDestCanvas()
480{
481 if (!m_dstImage) {
482 if (m_tilemapMode == TilemapMode::Tiles) {
483 m_dstImage.reset(Image::create(IMAGE_TILEMAP,
484 m_bounds.w, m_bounds.h, dst_buffer));
485 m_dstImage->setMaskColor(doc::notile);
486 }
487 else {
488 m_dstImage.reset(Image::create(m_sprite->pixelFormat(),
489 m_bounds.w, m_bounds.h, dst_buffer));
490 m_dstImage->setMaskColor(m_sprite->transparentColor());
491 }
492 m_dstImage->clear(m_dstImage->maskColor());
493 }
494 return m_dstImage.get();
495}
496
497Tileset* ExpandCelCanvas::getDestTileset()
498{
499 EXP_TRACE("ExpandCelCanvas::getDestTileset()"
500 "celCreated", m_celCreated,
501 "tilesetPreview", isTilesetPreview());
502
503 // When we edit the pixels in manual mode, we can create a tileset
504 // that will be used for preview purposes to see changes in all
505 // instances of the same tile.
506 if (!m_celCreated && isTilesetPreview()) {
507 // Copy the whole tileset
508 const Tileset* srcTileset = static_cast<LayerTilemap*>(m_layer)->tileset();
509
510 ASSERT(srcTileset);
511 m_dstTileset.reset(new Tileset(m_sprite,
512 srcTileset->grid(),
513 srcTileset->size()));
514 copySourceTilestToDestTileset();
515 }
516 return m_dstTileset.get();
517}
518
519void ExpandCelCanvas::validateSourceCanvas(const gfx::Region& rgn)
520{
521 EXP_TRACE("ExpandCelCanvas::validateSourceCanvas", rgn.bounds());
522
523 getSourceCanvas();
524
525 gfx::Region rgnToValidate;
526 gfx::Point origCelPos;
527 gfx::Point zeroPos;
528 if (m_tilemapMode == TilemapMode::Tiles) {
529 // Position of the tilemap cel inside the m_dstImage tilemap
530 origCelPos = m_grid.canvasToTile(m_origCelPos);
531 rgnToValidate = m_grid.canvasToTile(rgn);
532 }
533 else {
534 origCelPos = m_origCelPos;
535 rgnToValidate = rgn;
536 zeroPos = -m_bounds.origin();
537 }
538 EXP_TRACE(" ->", rgnToValidate.bounds());
539
540 rgnToValidate.offset(zeroPos);
541 rgnToValidate.createSubtraction(rgnToValidate, m_validSrcRegion);
542 rgnToValidate.createIntersection(rgnToValidate, gfx::Region(m_srcImage->bounds()));
543
544 if (m_celImage && previewSpecificLayerChanges()) {
545 gfx::Region rgnToClear;
546 rgnToClear.createSubtraction(
547 rgnToValidate,
548 gfx::Region(m_celImage->bounds()
549 .offset(origCelPos)
550 .offset(zeroPos)));
551 for (const auto& rc : rgnToClear)
552 fill_rect(m_srcImage.get(), rc, m_srcImage->maskColor());
553
554 if (m_celImage->pixelFormat() == IMAGE_TILEMAP &&
555 m_srcImage->pixelFormat() != IMAGE_TILEMAP) {
556 ASSERT(m_tilemapMode == TilemapMode::Pixels);
557
558 // For tilemaps, we can use the Render class to render visible
559 // tiles in the rgnToValidate of this cel.
560 render::Render subRender;
561 for (const auto& rc : rgnToValidate) {
562 subRender.renderCel(
563 m_srcImage.get(),
564 m_cel,
565 m_sprite,
566 m_celImage.get(),
567 m_layer,
568 m_sprite->palette(m_frame),
569 gfx::RectF(0, 0, m_bounds.w, m_bounds.h),
570 gfx::Clip(rc.x, rc.y,
571 rc.x+m_bounds.x-origCelPos.x,
572 rc.y+m_bounds.y-origCelPos.y, rc.w, rc.h),
573 255, BlendMode::NORMAL);
574 }
575 }
576 else if (m_celImage->pixelFormat() == IMAGE_TILEMAP &&
577 m_srcImage->pixelFormat() == IMAGE_TILEMAP) {
578 ASSERT(m_tilemapMode == TilemapMode::Tiles);
579
580 // We can copy the cel image directly
581 for (const auto& rc : rgnToValidate) {
582 m_srcImage->copy(
583 m_celImage.get(),
584 gfx::Clip(rc.x, rc.y,
585 rc.x-origCelPos.x,
586 rc.y-origCelPos.y, rc.w, rc.h));
587 }
588 }
589 else {
590 ASSERT(m_celImage->pixelFormat() != IMAGE_TILEMAP ||
591 m_tilemapMode == TilemapMode::Tiles);
592
593 // We can copy the cel image directly
594 for (const auto& rc : rgnToValidate)
595 m_srcImage->copy(
596 m_celImage.get(),
597 gfx::Clip(rc.x, rc.y,
598 rc.x+m_bounds.x-origCelPos.x,
599 rc.y+m_bounds.y-origCelPos.y, rc.w, rc.h));
600 }
601 }
602 else {
603 for (const auto& rc : rgnToValidate)
604 fill_rect(m_srcImage.get(), rc, m_srcImage->maskColor());
605 }
606
607 m_validSrcRegion.createUnion(m_validSrcRegion, rgnToValidate);
608}
609
610void ExpandCelCanvas::validateDestCanvas(const gfx::Region& rgn)
611{
612 EXP_TRACE("ExpandCelCanvas::validateDestCanvas", rgn.bounds());
613
614 Image* src;
615 int src_x, src_y;
616 if ((m_flags & NeedsSource) == NeedsSource) {
617 validateSourceCanvas(rgn);
618 src = m_srcImage.get();
619 src_x = m_bounds.x;
620 src_y = m_bounds.y;
621 }
622 else {
623 src = m_cel->image();
624 src_x = m_origCelPos.x;
625 src_y = m_origCelPos.y;
626 }
627
628 getDestCanvas(); // Create m_dstImage
629
630 gfx::Region rgnToValidate;
631 if (m_tilemapMode == TilemapMode::Tiles) {
632 for (const auto& rc : rgn)
633 rgnToValidate |= gfx::Region(m_grid.canvasToTile(rc));
634 }
635 else {
636 rgnToValidate = rgn;
637 }
638 EXP_TRACE(" ->", rgnToValidate.bounds());
639
640 if (m_tilemapMode != TilemapMode::Tiles)
641 rgnToValidate.offset(-m_bounds.origin());
642 rgnToValidate.createSubtraction(rgnToValidate, m_validDstRegion);
643 rgnToValidate.createIntersection(rgnToValidate, gfx::Region(m_dstImage->bounds()));
644
645 // ASSERT(src); // TODO is it always true?
646 if (src) {
647 gfx::Region rgnToClear;
648 rgnToClear.createSubtraction(rgnToValidate,
649 gfx::Region(src->bounds()
650 .offset(src_x, src_y)
651 .offset(-m_bounds.origin())));
652 for (const auto& rc : rgnToClear)
653 fill_rect(m_dstImage.get(), rc, m_dstImage->maskColor());
654
655 for (const auto& rc : rgnToValidate)
656 m_dstImage->copy(src,
657 gfx::Clip(rc.x, rc.y,
658 rc.x+m_bounds.x-src_x,
659 rc.y+m_bounds.y-src_y, rc.w, rc.h));
660 }
661 else {
662 for (const auto& rc : rgnToValidate)
663 fill_rect(m_dstImage.get(), rc, m_dstImage->maskColor());
664 }
665
666 m_validDstRegion.createUnion(m_validDstRegion, rgnToValidate);
667}
668
669void ExpandCelCanvas::validateDestTileset(const gfx::Region& rgn, const gfx::Region& forceRgn)
670{
671 EXP_TRACE("ExpandCelCanvas::validateDestTileset", rgn.bounds(), m_dstTileset);
672
673 // Update tiles from the valid dest image
674 if (m_dstTileset) {
675 gfx::Region regionToPatch = rgn;
676 modify_tilemap_cel_region(
677 m_cmds, m_cel,
678 m_dstTileset.get(),
679 regionToPatch,
680 m_tilesetMode,
681 [this](const doc::ImageRef& origTile,
682 const gfx::Rect& tileBoundsInCanvas) -> doc::ImageRef {
683 return trimDstImage(tileBoundsInCanvas);
684 },
685 forceRgn);
686 }
687}
688
689void ExpandCelCanvas::invalidateDestCanvas()
690{
691 EXP_TRACE("ExpandCelCanvas::invalidateDestCanvas");
692 m_validDstRegion.clear();
693
694 // Copy tileset for preview again
695 // TODO Is there a way to avoid copying tiles that weren't modified? comparing versions maybe?
696 if (m_dstTileset)
697 copySourceTilestToDestTileset();
698}
699
700void ExpandCelCanvas::invalidateDestCanvas(const gfx::Region& rgn)
701{
702 EXP_TRACE("ExpandCelCanvas::invalidateDestCanvas", rgn.bounds());
703
704 gfx::Region rgnToInvalidate(rgn);
705 rgnToInvalidate.offset(-m_bounds.origin());
706 m_validDstRegion.createSubtraction(m_validDstRegion, rgnToInvalidate);
707}
708
709void ExpandCelCanvas::copyValidDestToSourceCanvas(const gfx::Region& rgn)
710{
711 EXP_TRACE("ExpandCelCanvas::copyValidDestToSourceCanvas", rgn.bounds());
712
713 gfx::Region rgn2(rgn);
714 rgn2.offset(-m_bounds.origin());
715 rgn2.createIntersection(rgn2, m_validSrcRegion);
716 rgn2.createIntersection(rgn2, m_validDstRegion);
717 for (const auto& rc : rgn2)
718 m_srcImage->copy(m_dstImage.get(),
719 gfx::Clip(rc.x, rc.y, rc.x, rc.y, rc.w, rc.h));
720
721 // We cannot compare src vs dst in this case (e.g. on tools like
722 // spray and jumble that updated the source image from the modified
723 // destination).
724 m_canCompareSrcVsDst = false;
725}
726
727gfx::Rect ExpandCelCanvas::getTrimDstImageBounds() const
728{
729 if (m_layer->isBackground())
730 return m_dstImage->bounds();
731 else {
732 gfx::Rect bounds;
733 algorithm::shrink_bounds(m_dstImage.get(),
734 m_dstImage->maskColor(), m_layer, bounds);
735 return bounds;
736 }
737}
738
739ImageRef ExpandCelCanvas::trimDstImage(const gfx::Rect& bounds) const
740{
741 return ImageRef(
742 crop_image(m_dstImage.get(),
743 bounds.x-m_bounds.x,
744 bounds.y-m_bounds.y,
745 bounds.w, bounds.h,
746 m_dstImage->maskColor()));
747}
748
749void ExpandCelCanvas::copySourceTilestToDestTileset()
750{
751 ASSERT(m_layer->isTilemap());
752 const Tileset* srcTileset = static_cast<LayerTilemap*>(m_layer)->tileset();
753
754 for (tile_index i=0; i<srcTileset->size(); ++i) {
755 doc::copy_image(m_dstTileset->get(i).get(),
756 srcTileset->get(i).get());
757
758 // To rehash the tileset
759 m_dstTileset->notifyTileContentChange(i);
760 }
761}
762
763} // namespace app
764