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 | |
44 | namespace { |
45 | |
46 | // We cannot have two ExpandCelCanvas instances at the same time |
47 | // (because we share ImageBuffers between them). |
48 | static app::ExpandCelCanvas* singleton = nullptr; |
49 | |
50 | static doc::ImageBufferPtr src_buffer; |
51 | static doc::ImageBufferPtr dst_buffer; |
52 | |
53 | static void destroy_buffers() |
54 | { |
55 | src_buffer.reset(); |
56 | dst_buffer.reset(); |
57 | } |
58 | |
59 | static 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 | |
71 | namespace app { |
72 | |
73 | ExpandCelCanvas::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 | |
198 | ExpandCelCanvas::~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 | |
212 | void 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 | |
426 | void 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 | |
451 | gfx::Point ExpandCelCanvas::getCelOrigin() const |
452 | { |
453 | if (isTilesetPreview()) |
454 | return m_bounds.origin(); |
455 | else |
456 | return m_cel->position(); |
457 | } |
458 | |
459 | Image* 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 | |
479 | Image* 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 | |
497 | Tileset* 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 | |
519 | void 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 | |
610 | void 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 | |
669 | void 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 | |
689 | void 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 | |
700 | void 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 | |
709 | void 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 | |
727 | gfx::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 | |
739 | ImageRef 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 | |
749 | void 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 | |