diff --git a/src/app/tools/intertwiners.h b/src/app/tools/intertwiners.h index ccf1b1ad3..a6847b8bf 100644 --- a/src/app/tools/intertwiners.h +++ b/src/app/tools/intertwiners.h @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2018-2020 Igara Studio S.A. +// Copyright (C) 2018-2021 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -451,6 +451,8 @@ public: m_pts.reset(); } + int thirdFromLastPt = 0, nextPt = 0; + if (stroke.size() == 0) return; else if (stroke.size() == 1) { @@ -460,6 +462,13 @@ public: return; } else { + if (m_pts[m_pts.size() - 1] == stroke.lastPoint() && + stroke.firstPoint() == stroke.lastPoint()) + return; + + nextPt = m_pts.size(); + thirdFromLastPt = (m_pts.size() > 2 ? m_pts.size() - 3 : m_pts.size() - 1); + for (int c=0; c+1getBrush()->angle() == 0.0f || loop->getBrush()->angle() == 90.0f || loop->getBrush()->angle() == 180.0f))) { - for (int c=0; c 0 && c+1 < m_pts.size() @@ -488,18 +497,26 @@ 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]); + if (c == nextPt-1) + nextPt--; m_pts.erase(c); } } } - for (int c=0; csavePointshapeStrokePtArea(c, m_pts[c]); + } doPointshapeStrokePt(m_pts[c], loop); } } diff --git a/src/app/tools/tool_loop.h b/src/app/tools/tool_loop.h index d5651eb4d..cb6b6d8ea 100644 --- a/src/app/tools/tool_loop.h +++ b/src/app/tools/tool_loop.h @@ -11,6 +11,7 @@ #include "app/shade.h" #include "app/tools/dynamics.h" +#include "app/tools/stroke.h" #include "app/tools/tool_loop_modifiers.h" #include "app/tools/trace_policy.h" #include "doc/brush.h" @@ -251,6 +252,10 @@ namespace app { // Called when the user release the mouse on SliceInk virtual void onSliceRect(const gfx::Rect& bounds) = 0; + + virtual void savePointshapeStrokePtArea(const int pti, const Stroke::Pt& pt) = 0; + + virtual void restoreLastPts(const int pti, const tools::Stroke::Pt& pt) = 0; }; } // namespace tools diff --git a/src/app/tools/tool_loop_manager.cpp b/src/app/tools/tool_loop_manager.cpp index 80c10a626..d8e89db19 100644 --- a/src/app/tools/tool_loop_manager.cpp +++ b/src/app/tools/tool_loop_manager.cpp @@ -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 @@ -243,21 +243,6 @@ void ToolLoopManager::doLoopStep(bool lastStep) // (the final result is filled). m_toolLoop->invalidateDstImage(); } - else if (m_toolLoop->getTracePolicy() == TracePolicy::AccumulateUpdateLast) { - // Revalidate only this last dirty area (e.g. pixel-perfect - // freehand algorithm needs this trace policy to redraw only the - // last dirty area, which can vary in one pixel from the previous - // tool loop cycle). - if (m_toolLoop->getBrush()->type() != kImageBrushType) { - m_toolLoop->invalidateDstImage(m_dirtyArea); - } - // For custom brush we revalidate the whole destination area so - // the whole trace is redrawn from scratch. - else { - m_toolLoop->invalidateDstImage(); - m_toolLoop->validateDstImage(gfx::Region(m_toolLoop->getDstImage()->bounds())); - } - } m_toolLoop->validateDstImage(m_dirtyArea); diff --git a/src/app/tools/trace_policy.h b/src/app/tools/trace_policy.h index c8c91c2a1..24ae8e20b 100644 --- a/src/app/tools/trace_policy.h +++ b/src/app/tools/trace_policy.h @@ -1,4 +1,5 @@ // Aseprite +// Copyright (C) 2021 Igara Studio S.A. // Copyright (C) 2001-2016 David Capello // // This program is distributed under the terms of @@ -23,12 +24,6 @@ namespace app { // freehand like tools. Accumulate, - // It's like accumulate, but the last modified area in the - // destination is invalidated and redraw from the source image + - // tool trace. It's used by pixel-perfect freehand algorithm - // (because last modified pixels can differ). - AccumulateUpdateLast, - // Only the last trace is used. It means that on each ToolLoop // step, the destination image is completely invalidated and // restored from the source image. Used by diff --git a/src/app/ui/editor/tool_loop_impl.cpp b/src/app/ui/editor/tool_loop_impl.cpp index b6e7bb813..18911d67d 100644 --- a/src/app/ui/editor/tool_loop_impl.cpp +++ b/src/app/ui/editor/tool_loop_impl.cpp @@ -133,6 +133,21 @@ 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; + // Last point index. + int m_lastPti; + public: ToolLoopBase(Editor* editor, Site& site, const doc::Grid& grid, @@ -210,22 +225,18 @@ public: } #endif - if (m_tracePolicy == tools::TracePolicy::Accumulate || - m_tracePolicy == tools::TracePolicy::AccumulateUpdateLast) { + if (m_tracePolicy == tools::TracePolicy::Accumulate) { tools::ToolBox* toolbox = App::instance()->toolBox(); switch (params.freehandAlgorithm) { case tools::FreehandAlgorithm::DEFAULT: m_intertwine = toolbox->getIntertwinerById(tools::WellKnownIntertwiners::AsLines); - m_tracePolicy = tools::TracePolicy::Accumulate; break; case tools::FreehandAlgorithm::PIXEL_PERFECT: m_intertwine = toolbox->getIntertwinerById(tools::WellKnownIntertwiners::AsPixelPerfect); - m_tracePolicy = tools::TracePolicy::AccumulateUpdateLast; break; case tools::FreehandAlgorithm::DOTS: m_intertwine = toolbox->getIntertwinerById(tools::WellKnownIntertwiners::None); - m_tracePolicy = tools::TracePolicy::Accumulate; break; } @@ -421,6 +432,10 @@ public: void onSliceRect(const gfx::Rect& bounds) override { } + void savePointshapeStrokePtArea(const int pti, const tools::Stroke::Pt& pt) override { } + + void restoreLastPts(const int pti, const tools::Stroke::Pt& pt) override { } + #ifdef ENABLE_UI protected: void updateAllVisibleRegion() { @@ -726,8 +741,77 @@ public: m_internalCancel = true; } -#ifdef ENABLE_UI + // Saves the combined source image's areas and destination image's areas + // that will be updated by the last point of each stroke. 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 int pti, const tools::Stroke::Pt& pt) override { + if (m_savedAreas.size() > 0 && m_savedAreas[0].pos == pt) + return; + + m_savedAreas.clear(); + m_lastPti = pti; + + auto saveArea = [this](const tools::Stroke::Pt& pt) { + tools::Stroke::Pt pos = pt; + // By wrapping the stroke point position when tiled mode is active, the + // user can draw outside the canvas and still get the pixel-perfect + // effect. + wrapPositionOnTiledMode(pt, pos); + + gfx::Rect r; + getPointShape()->getModifiedArea(this, pos.x, pos.y, r); + + gfx::Region rgn(r); + m_editor->collapseRegionByTiledMode(rgn); + + for (auto a : rgn) { + ImageRef i(Image::create(getSrcImage()->pixelFormat(), a.w, a.h)); + i->copy(getSrcImage(), gfx::Clip(0, 0, a)); + i->copy(getDstImage(), gfx::Clip(0, 0, a)); + m_savedAreas.push_back(SavedArea{ i, pt, a}); + } + }; + + tools::Symmetry* symmetry = getSymmetry(); + if (symmetry) { + // Convert the point to the sprite position so we can apply the + // symmetry transformation. + tools::Stroke main_stroke; + main_stroke.addPoint(pt); + + tools::Strokes strokes; + symmetry->generateStrokes(main_stroke, strokes, this); + for (const auto& stroke : strokes) + saveArea(stroke[0]); + } + else + saveArea(pt); + } + + // 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; + + 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())); + } + } + private: + +#ifdef ENABLE_UI // EditorObserver impl void onScrollChanged(Editor* editor) override { updateAllVisibleRegion(); } void onZoomChanged(Editor* editor) override { updateAllVisibleRegion(); } @@ -744,6 +828,19 @@ private: } #endif // ENABLE_UI + void wrapPositionOnTiledMode(const tools::Stroke::Pt& pt, tools::Stroke::Pt& result) { + result = pt; + if (int(getTiledMode()) & int(TiledMode::X_AXIS)) { + result.x %= m_editor->canvasSize().w; + if (result.x < 0) + result.x += m_editor->canvasSize().w; + } + if (int(getTiledMode()) & int(TiledMode::Y_AXIS)) { + result.y %= m_editor->canvasSize().h; + if (result.y < 0) + result.y += m_editor->canvasSize().h; + } + } }; //////////////////////////////////////////////////////////////////////