diff --git a/src/app/tools/intertwine.h b/src/app/tools/intertwine.h index 03d34b0e1..82a87e6dd 100644 --- a/src/app/tools/intertwine.h +++ b/src/app/tools/intertwine.h @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2019-2020 Igara Studio S.A. +// Copyright (C) 2019-2021 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -26,7 +26,7 @@ namespace app { public: virtual ~Intertwine() { } virtual bool snapByAngle() { return false; } - virtual void prepareIntertwine() { } + virtual void prepareIntertwine(ToolLoop* loop) { } // The given stroke must be relative to the cel origin. virtual void joinStroke(ToolLoop* loop, const Stroke& stroke) = 0; @@ -34,6 +34,11 @@ namespace app { virtual gfx::Rect getStrokeBounds(ToolLoop* loop, const Stroke& stroke); + // Special region to force when the modify_tilemap_cel_region() + // is called to restore the m_dstTileset from the m_dstImage + // in ExpandCelCanvas::validateDestTileset. + virtual gfx::Region forceTilemapRegionToValidate() { return gfx::Region(); } + struct LineData { ToolLoop* loop; Stroke::Pt a, b, pt; diff --git a/src/app/tools/intertwiners.h b/src/app/tools/intertwiners.h index 72da75b09..4531b668c 100644 --- a/src/app/tools/intertwiners.h +++ b/src/app/tools/intertwiners.h @@ -6,6 +6,7 @@ // the End-User License Agreement for Aseprite. #include "base/pi.h" +#include "doc/layer_tilemap.h" namespace app { namespace tools { @@ -94,7 +95,7 @@ class IntertwineAsLines : public Intertwine { public: bool snapByAngle() override { return true; } - void prepareIntertwine() override { + void prepareIntertwine(ToolLoop* loop) override { m_retainedTracePolicyLast = false; m_firstStroke = true; } @@ -431,14 +432,48 @@ class IntertwineAsPixelPerfect : public Intertwine { Stroke m_pts; bool m_saveStrokeArea = false; + // Helper struct to store an image's area that will be affected by the stroke + // point at the specified position of the original image. + struct SavedArea { + doc::ImageRef img; + // Original stroke point position. + tools::Stroke::Pt pos; + // Area of the original image that was saved into img. + gfx::Rect r; + }; + // Holds the areas saved by savePointshapeStrokePtArea method and restored by + // restoreLastPts method. + std::vector m_savedAreas; + // When a SavedArea is restored we add its Rect to this Region, then we use + // this to expand the modified region when editing a tilemap manually. + gfx::Region m_restoredRegion; + // Last point index. + int m_lastPti; + + // Temporal tileset with latest changes to be used by pixel perfect only when + // modifying a tilemap in Manual mode. + std::unique_ptr m_tempTileset; + doc::Grid m_grid; + public: // Useful for Shift+Ctrl+pencil to draw straight lines and snap // angle when "pixel perfect" is selected. bool snapByAngle() override { return true; } - void prepareIntertwine() override { + void prepareIntertwine(ToolLoop* loop) override { m_pts.reset(); m_retainedTracePolicyLast = false; + m_grid = loop->getGrid(); + + if (loop->getLayer()->isTilemap() && + !loop->isTilemapMode() && + loop->isManualTilesetMode()) { + const Tileset* srcTileset = static_cast(loop->getLayer())->tileset(); + m_tempTileset.reset(Tileset::MakeCopyCopyingImages(srcTileset)); + } + else { + m_tempTileset.reset(); + } } void joinStroke(ToolLoop* loop, const Stroke& stroke) override { @@ -500,7 +535,7 @@ public: && (m_pts[c+1].x == m_pts[c].x || m_pts[c+1].y == m_pts[c].y) && m_pts[c-1].x != m_pts[c+1].x && m_pts[c-1].y != m_pts[c+1].y) { - loop->restoreLastPts(c, m_pts[c]); + restoreLastPts(loop, c, m_pts[c]); if (c == nextPt-1) nextPt--; m_pts.erase(c); @@ -522,8 +557,8 @@ public: // use it in doTransformPoint. m_saveStrokeArea = (c == m_pts.size() - 1 && !m_retainedTracePolicyLast); if (m_saveStrokeArea) { - loop->clearPointshapeStrokePtAreas(); - loop->setLastPtIndex(c); + clearPointshapeStrokePtAreas(); + setLastPtIndex(c); } doPointshapeStrokePt(m_pts[c], loop); } @@ -542,17 +577,172 @@ public: loop, (AlgoHLine)doPointshapeHline); } + gfx::Region forceTilemapRegionToValidate() override { + return m_restoredRegion; + } + protected: void doTransformPoint(const Stroke::Pt& pt, ToolLoop* loop) override { if (m_saveStrokeArea) - loop->savePointshapeStrokePtArea(pt); + savePointshapeStrokePtArea(loop, pt); Intertwine::doTransformPoint(pt, loop); if (loop->getLayer()->isTilemap()) { - loop->updateTempTileset(pt); + updateTempTileset(loop, pt); } } + +private: + void clearPointshapeStrokePtAreas() { + m_savedAreas.clear(); + } + + void setLastPtIndex(const int pti) { + m_lastPti = pti; + } + + // Saves the destination image's area that will be updated by the point + // passed. The idea is to have the state of the image (only the + // portion modified by the stroke's point shape) before drawing the last + // point of the stroke, then if that point has to be deleted by the + // pixel-perfect algorithm, we can use this image to restore the image to the + // state previous to the deletion. This method is used by + // IntertwineAsPixelPerfect.joinStroke() method. + void savePointshapeStrokePtArea(ToolLoop* loop, const tools::Stroke::Pt& pt) { + gfx::Rect r; + loop->getPointShape()->getModifiedArea(loop, pt.x, pt.y, r); + + gfx::Region rgn(r); + // By wrapping the modified area's position when tiled mode is active, the + // user can draw outside the canvas and still get the pixel-perfect + // effect. + loop->getTiledModeHelper().wrapPosition(rgn); + loop->getTiledModeHelper().collapseRegionByTiledMode(rgn); + + for (auto a : rgn) { + a.offset(-loop->getCelOrigin()); + + if (m_tempTileset) { + forEachTilePos( + loop, m_grid.tilesInCanvasRegion(gfx::Region(a)), + [loop](const doc::ImageRef existentTileImage, + const gfx::Point tilePos) { + loop->getDstImage()->copy( + existentTileImage.get(), + gfx::Clip(tilePos.x, tilePos.y, 0, 0, + existentTileImage.get()->width(), + existentTileImage.get()->height())); + }); + } + + ImageRef i(Image::create(loop->getDstImage()->pixelFormat(), a.w, a.h)); + i->copy(loop->getDstImage(), gfx::Clip(0, 0, a)); + m_savedAreas.push_back(SavedArea{ i, pt, a }); + } + } + + // Takes the images saved by savePointshapeStrokePtArea and copies them to + // the destination image. It restores the destination image because the + // images in m_savedAreas are from previous states of the destination + // image. This method is used by IntertwineAsPixelPerfect.joinStroke() + // method. + void restoreLastPts(ToolLoop* loop, const int pti, const tools::Stroke::Pt& pt) { + if (m_savedAreas.empty() || pti != m_lastPti || m_savedAreas[0].pos != pt) + return; + + m_restoredRegion.clear(); + + tools::Stroke::Pt pos; + for (int i=0; igetDstImage()->copy( + m_savedAreas[i].img.get(), + gfx::Clip(m_savedAreas[i].r.origin(), + m_savedAreas[i].img->bounds())); + + if (m_tempTileset) { + auto r = m_savedAreas[i].r; + forEachTilePos( + loop, m_grid.tilesInCanvasRegion(gfx::Region(r)), + [this, i, r](const doc::ImageRef existentTileImage, + const gfx::Point tilePos) { + existentTileImage->copy( + m_savedAreas[i].img.get(), + gfx::Clip(r.x - tilePos.x, + r.y - tilePos.y, + 0, 0, r.w, r.h)); + }); + } + + m_restoredRegion |= gfx::Region(m_savedAreas[i].r); + } + } + + void updateTempTileset(ToolLoop* loop, const tools::Stroke::Pt& pt) { + if (!m_tempTileset) + return; + + gfx::Rect r; + loop->getPointShape()->getModifiedArea(loop, pt.x, pt.y, r); + + auto tilesPts = m_grid.tilesInCanvasRegion(gfx::Region(r)); + forEachTilePos( + loop, tilesPts, + [loop, r](const doc::ImageRef existentTileImage, + const gfx::Point tilePos) { + existentTileImage->copy( + loop->getDstImage(), + gfx::Clip(r.x - tilePos.x, + r.y - tilePos.y, r)); + }); + + if (tilesPts.size() > 1) { + forEachTilePos( + loop, tilesPts, + [loop](const doc::ImageRef existentTileImage, + const gfx::Point tilePos) { + loop->getDstImage()->copy( + existentTileImage.get(), + gfx::Clip(tilePos.x, tilePos.y, 0, 0, + existentTileImage.get()->width(), + existentTileImage.get()->height())); + }); + } + } + + // Loops over the points in tilesPts, and for each one calls the provided + // processTempTileImage callback passing to it the corresponding temp tile + // image and canvas position. + void forEachTilePos(ToolLoop* loop, + const std::vector& tilesPts, + const std::function& processTempTileImage) { + ASSERT(loop->getCel()); + if (!loop->getCel()) + return; + + const Image* tilemapImage = loop->getCel()->image(); + for (const gfx::Point& tilePt : tilesPts) { + // Ignore modifications outside the tilemap + if (!tilemapImage->bounds().contains(tilePt.x, tilePt.y)) + continue; + + const doc::tile_t t = tilemapImage->getPixel(tilePt.x, tilePt.y); + if (t == doc::notile) + continue; + + const doc::tile_index ti = doc::tile_geti(t); + const doc::ImageRef existentTileImage = m_tempTileset->get(ti); + if (!existentTileImage) { + continue; + } + + auto tilePos = m_grid.tileToCanvas(tilePt); + + processTempTileImage(existentTileImage, tilePos); + } + } + }; } // namespace tools diff --git a/src/app/tools/tool_loop.h b/src/app/tools/tool_loop.h index 6658c8d37..1fe424152 100644 --- a/src/app/tools/tool_loop.h +++ b/src/app/tools/tool_loop.h @@ -90,9 +90,13 @@ namespace app { // Returns the layer that will be modified if the tool paints virtual Layer* getLayer() = 0; + virtual const Cel* getCel() = 0; + // Returns true if the current mode is TileMap (false = Pixels) virtual bool isTilemapMode() = 0; + virtual bool isManualTilesetMode() const = 0; + // Returns the frame where we're paiting virtual frame_t getFrame() = 0; @@ -254,13 +258,6 @@ namespace app { // Called when the user release the mouse on SliceInk virtual void onSliceRect(const gfx::Rect& bounds) = 0; - // The following functions are used in pixel perfect mode - virtual void clearPointshapeStrokePtAreas() = 0; - virtual void setLastPtIndex(const int pti) = 0; - virtual void savePointshapeStrokePtArea(const Stroke::Pt& pt) = 0; - virtual void restoreLastPts(const int pti, const tools::Stroke::Pt& pt) = 0; - virtual void updateTempTileset(const tools::Stroke::Pt& pt) = 0; - virtual const app::TiledModeHelper& getTiledModeHelper() = 0; }; diff --git a/src/app/tools/tool_loop_manager.cpp b/src/app/tools/tool_loop_manager.cpp index 488de1631..501517324 100644 --- a/src/app/tools/tool_loop_manager.cpp +++ b/src/app/tools/tool_loop_manager.cpp @@ -79,7 +79,7 @@ void ToolLoopManager::prepareLoop(const Pointer& pointer) // Prepare the ink m_toolLoop->getInk()->prepareInk(m_toolLoop); m_toolLoop->getController()->prepareController(m_toolLoop); - m_toolLoop->getIntertwine()->prepareIntertwine(); + m_toolLoop->getIntertwine()->prepareIntertwine(m_toolLoop); m_toolLoop->getPointShape()->preparePointShape(m_toolLoop); } diff --git a/src/app/ui/editor/brush_preview.cpp b/src/app/ui/editor/brush_preview.cpp index 950847dec..af7fa68b1 100644 --- a/src/app/ui/editor/brush_preview.cpp +++ b/src/app/ui/editor/brush_preview.cpp @@ -326,7 +326,7 @@ void BrushPreview::show(const gfx::Point& screenPos) if (loop) { loop->getInk()->prepareInk(loop.get()); loop->getController()->prepareController(loop.get()); - loop->getIntertwine()->prepareIntertwine(); + loop->getIntertwine()->prepareIntertwine(loop.get()); loop->getPointShape()->preparePointShape(loop.get()); tools::Stroke::Pt pt(brushBounds.x-origBrushBounds.x, diff --git a/src/app/ui/editor/tool_loop_impl.cpp b/src/app/ui/editor/tool_loop_impl.cpp index 9b669aa38..2c0756e46 100644 --- a/src/app/ui/editor/tool_loop_impl.cpp +++ b/src/app/ui/editor/tool_loop_impl.cpp @@ -28,6 +28,7 @@ #include "app/tools/controller.h" #include "app/tools/freehand_algorithm.h" #include "app/tools/ink.h" +#include "app/tools/intertwine.h" #include "app/tools/point_shape.h" #include "app/tools/symmetry.h" #include "app/tools/tool.h" @@ -96,6 +97,7 @@ protected: Sprite* m_sprite; Layer* m_layer; frame_t m_frame; + TilesetMode m_tilesetMode; RgbMap* m_rgbMap; DocumentPreferences& m_docPref; ToolPreferences& m_toolPref; @@ -133,24 +135,6 @@ protected: // given document. gfx::Region m_allVisibleRgn; - // Helper struct to store an image's area that will be affected by the stroke - // point at the specified position of the original image. - struct SavedArea { - doc::ImageRef img; - // Original stroke point position. - tools::Stroke::Pt pos; - // Area of the original image that was saved into img. - gfx::Rect r; - }; - // Holds the areas saved by savePointshapeStrokePtArea method and restored by - // restoreLastPts method. - std::vector m_savedAreas; - // When a SavedArea is restored we add its Rect to this Region, then we use - // this to expand the modified region when editing a tilemap manually. - gfx::Region m_restoredRegion; - // Last point index. - int m_lastPti; - app::TiledModeHelper m_tiledModeHelper; public: @@ -166,6 +150,7 @@ public: , m_sprite(site.sprite()) , m_layer(site.layer()) , m_frame(site.frame()) + , m_tilesetMode(site.tilesetMode()) , m_rgbMap(nullptr) , m_docPref(Preferences::instance().document(m_document)) , m_toolPref(Preferences::instance().tool(m_tool)) @@ -304,7 +289,9 @@ public: Doc* getDocument() override { return m_document; } Sprite* sprite() override { return m_sprite; } Layer* getLayer() override { return m_layer; } + const Cel* getCel() override { return nullptr; } bool isTilemapMode() override { return m_tilesMode; }; + bool isManualTilesetMode() const override { return m_tilesetMode == TilesetMode::Manual; }; frame_t getFrame() override { return m_frame; } RgbMap* getRgbMap() override { if (!m_rgbMap) { @@ -438,16 +425,6 @@ public: void onSliceRect(const gfx::Rect& bounds) override { } - void clearPointshapeStrokePtAreas() override { } - - void setLastPtIndex(const int pti) override { } - - void savePointshapeStrokePtArea(const tools::Stroke::Pt& pt) override { } - - void restoreLastPts(const int pti, const tools::Stroke::Pt& pt) override { } - - void updateTempTileset(const tools::Stroke::Pt& pt) override { } - const app::TiledModeHelper& getTiledModeHelper() override { return m_tiledModeHelper; } @@ -489,9 +466,6 @@ class ToolLoopImpl : public ToolLoopBase, std::unique_ptr m_expandCelCanvas; Image* m_floodfillSrcImage; bool m_saveLastPoint; - // Temporal tileset with latest changes to be used by pixel perfect only when - // modifying a tilemap in Manual mode. - std::unique_ptr m_tempTileset; public: ToolLoopImpl(Editor* editor, @@ -512,7 +486,6 @@ public: ModifyDocument)) , m_floodfillSrcImage(nullptr) , m_saveLastPoint(saveLastPoint) - , m_tempTileset(nullptr) { if (m_pointShape->isFloodFill()) { if (m_tilesMode) { @@ -608,11 +581,6 @@ public: if (m_editor) m_editor->add_observer(this); #endif - - if (m_layer->isTilemap() && site.tilesetMode() == TilesetMode::Manual) { - const Tileset* srcTileset = static_cast(m_layer)->tileset(); - m_tempTileset.reset(Tileset::MakeCopyCopyingImages(srcTileset)); - } } ~ToolLoopImpl() { @@ -700,6 +668,7 @@ public: #endif } + const Cel* getCel() override { return m_expandCelCanvas->getCel(); } const Image* getSrcImage() override { return m_expandCelCanvas->getSourceCanvas(); } const Image* getFloodFillSrcImage() override { return m_floodfillSrcImage; } Image* getDstImage() override { return m_expandCelCanvas->getDestCanvas(); } @@ -711,7 +680,8 @@ public: m_expandCelCanvas->validateDestCanvas(rgn); } void validateDstTileset(const gfx::Region& rgn) override { - m_expandCelCanvas->validateDestTileset(rgn, m_restoredRegion); + m_expandCelCanvas->validateDestTileset( + rgn, getIntertwine()->forceTilemapRegionToValidate()); } void invalidateDstImage() override { m_expandCelCanvas->invalidateDestCanvas(); @@ -766,141 +736,8 @@ public: m_internalCancel = true; } - void clearPointshapeStrokePtAreas() override { - m_savedAreas.clear(); - } - - void setLastPtIndex(const int pti) override { - m_lastPti = pti; - } - - // Saves the destination image's area that will be updated by the point - // passed. The idea is to have the state of the image (only the - // portion modified by the stroke's point shape) before drawing the last - // point of the stroke, then if that point has to be deleted by the - // pixel-perfect algorithm, we can use this image to restore the image to the - // state previous to the deletion. This method is used by - // IntertwineAsPixelPerfect.joinStroke() method. - void savePointshapeStrokePtArea(const tools::Stroke::Pt& pt) override { - gfx::Rect r; - getPointShape()->getModifiedArea(this, pt.x, pt.y, r); - - gfx::Region rgn(r); - // By wrapping the modified area's position when tiled mode is active, the - // user can draw outside the canvas and still get the pixel-perfect - // effect. - m_tiledModeHelper.wrapPosition(rgn); - m_tiledModeHelper.collapseRegionByTiledMode(rgn); - - for (auto a : rgn) { - a.offset(-m_celOrigin); - - if (m_tempTileset) { - forEachTilePos( - m_grid.tilesInCanvasRegion(gfx::Region(a)), - [this](const doc::ImageRef existentTileImage, - const gfx::Point tilePos) { - getDstImage()->copy(existentTileImage.get(), - gfx::Clip(tilePos.x, tilePos.y, 0, 0, - existentTileImage.get()->width(), - existentTileImage.get()->height())); - }); - } - - ImageRef i(Image::create(getDstImage()->pixelFormat(), a.w, a.h)); - i->copy(getDstImage(), gfx::Clip(0, 0, a)); - m_savedAreas.push_back(SavedArea{ i, pt, a}); - } - } - - // Takes the images saved by savePointshapeStrokePtArea and copies them to - // the destination image. It restores the destination image because the - // images in m_savedAreas are from previous states of the destination - // image. This method is used by IntertwineAsPixelPerfect.joinStroke() - // method. - void restoreLastPts(const int pti, const tools::Stroke::Pt& pt) override { - if (m_savedAreas.empty() || pti != m_lastPti || m_savedAreas[0].pos != pt) - return; - - m_restoredRegion.clear(); - - tools::Stroke::Pt pos; - for (int i=0; icopy(m_savedAreas[i].img.get(), - gfx::Clip(m_savedAreas[i].r.origin(), - m_savedAreas[i].img->bounds())); - - if (m_tempTileset) { - auto r = m_savedAreas[i].r; - forEachTilePos( - m_grid.tilesInCanvasRegion(gfx::Region(r)), - [this, i, r](const doc::ImageRef existentTileImage, - const gfx::Point tilePos) { - existentTileImage->copy(m_savedAreas[i].img.get(), gfx::Clip(r.x - tilePos.x, r.y - tilePos.y, 0, 0, r.w, r.h)); - }); - } - - m_restoredRegion |= gfx::Region(m_savedAreas[i].r); - } - } - - void updateTempTileset(const tools::Stroke::Pt& pt) override { - if (!m_tempTileset) - return; - - gfx::Rect r; - getPointShape()->getModifiedArea(this, pt.x, pt.y, r); - - auto tilesPts = m_grid.tilesInCanvasRegion(gfx::Region(r)); - forEachTilePos( - tilesPts, - [this, r](const doc::ImageRef existentTileImage, - const gfx::Point tilePos) { - existentTileImage->copy(getDstImage(), gfx::Clip(r.x - tilePos.x, r.y - tilePos.y, r.x, r.y, r.w, r.h)); - }); - - if (tilesPts.size() > 1) { - forEachTilePos( - tilesPts, - [this](const doc::ImageRef existentTileImage, - const gfx::Point tilePos) { - getDstImage()->copy(existentTileImage.get(), gfx::Clip(tilePos.x, tilePos.y, 0, 0, - existentTileImage.get()->width(), - existentTileImage.get()->height())); - }); - } - } - private: - // Loops over the points in tilesPts, and for each one calls the provided - // processTempTileImage callback passing to it the corresponding temp tile - // image and canvas position. - void forEachTilePos(const std::vector& tilesPts, - const std::function& processTempTileImage) { - auto cel = m_expandCelCanvas->getCel(); - for (const gfx::Point& tilePt : tilesPts) { - // Ignore modifications outside the tilemap - if (!cel->image()->bounds().contains(tilePt.x, tilePt.y)) - continue; - - const doc::tile_t t = cel->image()->getPixel(tilePt.x, tilePt.y); - if (t == doc::notile) - continue; - - const doc::tile_index ti = doc::tile_geti(t); - const doc::ImageRef existentTileImage = m_tempTileset->get(ti); - if (!existentTileImage) { - continue; - } - - auto tilePos = m_grid.tileToCanvas(tilePt); - - processTempTileImage(existentTileImage, tilePos); - } - } - #ifdef ENABLE_UI // EditorObserver impl void onScrollChanged(Editor* editor) override { updateAllVisibleRegion(); }