Add Brush::clone() functions to fix/simplify some Brush-related code

This refactor includes:

- In Lua now we can clone a custom brush with Brush(Image) and the new
  brush doesn't share the image with the original one (added a new test
  for this).

- Avoid creating extra images when it's not needed using
  Brush::cloneWithExistingImages() (we can inject existing images in
  the brush itself).

- Delete Brush-copy contructor & assign operator to use
  Brush::clone() functions instead (which are more explicit).

- Some code from 12d8135264 (#4023)
  reverted to avoid recreating brushes on left-click or in the brush
  preview, i.e. moving the mouse (#4013 refers only to right-click, so
  only on right-click we have to adjust the custom brush).
This commit is contained in:
David Capello 2024-05-03 11:35:36 -03:00
parent 12d8135264
commit 92edd5f700
9 changed files with 143 additions and 72 deletions

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (c) 2023 Igara Studio S.A.
// Copyright (c) 2023-2024 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
@ -156,9 +156,7 @@ void ChangeBrushCommand::onExecute(Context* context)
// Create a copy of the brush (to avoid modifying the original
// brush from the AppBrushes stock)
BrushRef newBrush = std::make_shared<Brush>(*brush);
newBrush->setImage(newImg.get(),
newMsk.get());
BrushRef newBrush = brush->cloneWithExistingImages(newImg, newMsk);
contextBar->setActiveBrush(newBrush);
}
else {
@ -210,9 +208,7 @@ void ChangeBrushCommand::onExecute(Context* context)
break;
}
BrushRef newBrush = std::make_shared<Brush>(*brush);
newBrush->setImage(newImg.get(),
newMsk.get());
BrushRef newBrush = brush->cloneWithExistingImages(newImg, newMsk);
contextBar->setActiveBrush(newBrush);
}
else {
@ -297,10 +293,7 @@ void ChangeBrushCommand::onExecute(Context* context)
ImageRef newImg2(crop_image(newImg.get(), cropBounds, bg));
ImageRef newMsk2(crop_image(newMsk.get(), cropBounds, bg));
BrushRef newBrush = std::make_shared<Brush>(*brush);
newBrush->setImage(newImg.get(),
newMsk.get());
BrushRef newBrush = brush->cloneWithExistingImages(newImg2, newMsk2);
contextBar->setActiveBrush(newBrush);
}
break;

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2020 Igara Studio S.A.
// Copyright (C) 2019-2024 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -36,7 +36,7 @@ BrushRef Brush_new(lua_State* L, int index)
if (auto brush2 = may_get_obj<BrushObj>(L, index)) {
ASSERT(brush2->brush);
if (brush2->brush)
brush.reset(new Brush(*brush2->brush));
brush = brush2->brush->cloneWithNewImages();
}
else if (auto image = may_get_image_from_arg(L, index)) {
if (image) {

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2023 Igara Studio S.A.
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -44,7 +44,8 @@ using namespace filters;
ToolLoopManager::ToolLoopManager(ToolLoop* toolLoop)
: m_toolLoop(toolLoop)
, m_canceled(false)
, m_brush0(*toolLoop->getBrush())
, m_brushSize0(toolLoop->getBrush()->size())
, m_brushAngle0(toolLoop->getBrush()->angle())
, m_dynamics(toolLoop->getDynamics())
{
}
@ -358,8 +359,8 @@ Stroke::Pt ToolLoopManager::getSpriteStrokePt(const Pointer& pointer)
{
// Convert the screen point to a sprite point
Stroke::Pt spritePoint = pointer.point();
spritePoint.size = m_brush0.size();
spritePoint.angle = m_brush0.angle();
spritePoint.size = m_brushSize0;
spritePoint.angle = m_brushAngle0;
// Center the input to some grid point if needed
snapToGrid(spritePoint);

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2021 Igara Studio S.A.
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
@ -95,7 +95,8 @@ private:
Pointer m_lastPointer;
gfx::Region m_dirtyArea;
gfx::Region m_nextDirtyArea;
doc::Brush m_brush0;
const int m_brushSize0;
const int m_brushAngle0;
DynamicsOptions m_dynamics;
gfx::PointF m_stabilizerCenter;
};

View File

@ -467,7 +467,9 @@ os::SurfaceRef BrushPopup::createSurfaceForBrush(const BrushRef& origBrush,
BrushRef brush = origBrush;
if (brush) {
if (brush->type() != kImageBrushType && brush->size() > kMaxSize) {
brush.reset(new Brush(*brush));
// Clone with shared images, as setSize() will re-create the
// images and the brush is no kImageBrushType anyway.
brush = brush->cloneWithSharedImages();
brush->setSize(kMaxSize);
}
// Show the original image in the popup (without the image colors

View File

@ -190,16 +190,31 @@ public:
ASSERT(m_ink);
ASSERT(m_controller);
if (m_brush->type() == kImageBrushType &&
(m_button == Right || (m_button == Left && m_brush->isMonochromeImage()))) {
m_brush->setImageColor(
Brush::ImageColor::MainColor,
// If the user right-clicks with a custom/image brush we change
// the image's colors of the brush to the background color.
//
// This is different from SwitchColors that makes a new brush
// switching fg <-> bg colors, so here we have some extra
// functionality with custom brushes (quickly convert the custom
// brush with a plain color, or in other words, replace the custom
// brush area with the background color).
if (m_brush->type() == kImageBrushType && m_button == Right) {
// We've to recalculate the background color to use for the
// brush using the specific brush image pixel format/color mode,
// as we cannot use m_primaryColor or m_bgColor here because
// those are in the sprite pixel format/color mode.
const color_t brushColor =
color_utils::color_for_target_mask(
(m_button == Left ? Preferences::instance().colorBar.fgColor() :
Preferences::instance().colorBar.bgColor()),
Preferences::instance().colorBar.bgColor(),
ColorTarget(ColorTarget::TransparentLayer,
m_brush->image()->pixelFormat(),
-1)));
-1));
// Clone the brush with new images to avoid modifying the
// current brush used in left-click / brush preview.
BrushRef newBrush = m_brush->cloneWithNewImages();
newBrush->setImageColor(Brush::ImageColor::BothColors, brushColor);
m_brush = newBrush;
}
if (m_tilesMode) {

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (C) 2019-2023 Igara Studio S.A.
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2001-2016 David Capello
//
// This file is released under the terms of the MIT license.
@ -48,32 +48,43 @@ Brush::Brush(BrushType type, int size, int angle)
regenerate();
}
Brush::Brush(const Brush& brush)
{
m_type = brush.m_type;
m_size = brush.m_size;
m_angle = brush.m_angle;
m_image = brush.m_image;
m_maskBitmap = brush.m_maskBitmap;
m_pattern = brush.m_pattern;
m_patternOrigin = brush.m_patternOrigin;
m_gen = 0;
regenerate();
}
Brush::~Brush()
{
clean();
}
void Brush::setType(BrushType type)
BrushRef Brush::cloneWithSharedImages() const
{
m_type = type;
if (m_type != kImageBrushType)
regenerate();
BrushRef newBrush = std::make_shared<Brush>();
newBrush->copyFieldsFromBrush(*this);
return newBrush;
}
BrushRef Brush::cloneWithNewImages() const
{
BrushRef newBrush = std::make_shared<Brush>();
newBrush->copyFieldsFromBrush(*this);
if (newBrush->m_image)
newBrush->m_image.reset(Image::createCopy(newBrush->m_image.get()));
if (newBrush->m_maskBitmap)
newBrush->m_maskBitmap.reset(Image::createCopy(newBrush->m_maskBitmap.get()));
return newBrush;
}
BrushRef Brush::cloneWithExistingImages(const ImageRef& image,
const ImageRef& maskBitmap) const
{
BrushRef newBrush = std::make_shared<Brush>();
newBrush->copyFieldsFromBrush(*this);
newBrush->m_image = image;
if (maskBitmap)
newBrush->m_maskBitmap = maskBitmap;
else
clean();
newBrush->regenerateMaskBitmap();
newBrush->resetBounds();
return newBrush;
}
void Brush::setSize(int size)
@ -95,16 +106,8 @@ void Brush::setImage(const Image* image,
m_image.reset(Image::createCopy(image));
if (maskBitmap)
m_maskBitmap.reset(Image::createCopy(maskBitmap));
else {
int w = image->width();
int h = image->height();
m_maskBitmap.reset(Image::create(IMAGE_BITMAP, w, h));
LockImageBits<BitmapTraits> bits(m_maskBitmap.get());
auto pos = bits.begin();
for (int v=0; v<h; ++v)
for (int u=0; u<w; ++u, ++pos)
*pos = (get_pixel(image, u, v) != image->maskColor());
}
else
regenerateMaskBitmap();
m_backupImage.reset();
m_mainColor.reset();
@ -234,7 +237,8 @@ static void replace_image_colors_indexed(
}
}
void Brush::setImageColor(ImageColor imageColor, color_t color)
void Brush::setImageColor(const ImageColor imageColor,
const color_t color)
{
ASSERT(m_image);
if (!m_image)
@ -249,10 +253,13 @@ void Brush::setImageColor(ImageColor imageColor, color_t color)
switch (imageColor) {
case ImageColor::MainColor:
m_mainColor = color_t(color);
m_mainColor = color;
break;
case ImageColor::BackgroundColor:
m_bgColor = color_t(color);
m_bgColor = color;
break;
case ImageColor::BothColors:
m_mainColor = m_bgColor = color;
break;
}
@ -379,6 +386,22 @@ void Brush::regenerate()
}
}
void Brush::regenerateMaskBitmap()
{
ASSERT(m_image);
if (!m_image)
return;
int w = m_image->width();
int h = m_image->height();
m_maskBitmap.reset(Image::create(IMAGE_BITMAP, w, h));
LockImageBits<BitmapTraits> bits(m_maskBitmap.get());
auto pos = bits.begin();
for (int v=0; v<h; ++v)
for (int u=0; u<w; ++u, ++pos)
*pos = (get_pixel(m_image.get(), u, v) != m_image->maskColor());
}
void Brush::resetBounds()
{
m_center = gfx::Point(std::max(0, m_image->width()/2),
@ -388,4 +411,19 @@ void Brush::resetBounds()
m_image->height()));
}
void Brush::copyFieldsFromBrush(const Brush& brush)
{
m_type = brush.m_type;
m_size = brush.m_size;
m_angle = brush.m_angle;
m_image = brush.m_image;
m_maskBitmap = brush.m_maskBitmap;
m_bounds = brush.m_bounds;
m_center = brush.m_center;
m_pattern = brush.m_pattern;
m_patternOrigin = brush.m_patternOrigin;
m_patternImage = brush.m_patternImage;
m_gen = 0;
}
} // namespace doc

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (C) 2019-2023 Igara Studio S.A.
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This file is released under the terms of the MIT license.
@ -22,18 +22,34 @@
namespace doc {
class Brush;
using BrushRef = std::shared_ptr<Brush>;
class Brush {
public:
static const int kMinBrushSize = 1;
static const int kMaxBrushSize = 64;
enum class ImageColor { MainColor, BackgroundColor };
enum class ImageColor { MainColor, BackgroundColor, BothColors };
Brush();
Brush(BrushType type, int size, int angle);
Brush(const Brush& brush);
~Brush();
// Don't offer copy constructor/operator, use clone*() functions
// instead.
Brush(const Brush&) = delete;
Brush& operator=(const Brush&) = delete;
// Cloned brushes can share the same image until
// setSize()/Angle()/etc. (regenerate()) is called for the new
// brush. In that case the original brush and the cloned one will
// have a different image after all.
BrushRef cloneWithSharedImages() const;
BrushRef cloneWithNewImages() const;
BrushRef cloneWithExistingImages(const ImageRef& image,
const ImageRef& maskBitmap) const;
BrushType type() const { return m_type; }
int size() const { return m_size; }
int angle() const { return m_angle; }
@ -48,7 +64,6 @@ namespace doc {
const gfx::Rect& bounds() const { return m_bounds; }
const gfx::Point& center() const { return m_center; }
void setType(BrushType type);
void setSize(int size);
void setAngle(int angle);
void setImage(const Image* image,
@ -78,14 +93,12 @@ namespace doc {
return m_image.get();
}
const bool isMonochromeImage() const {
return m_mainColor.has_value();
}
private:
void clean();
void regenerate();
void regenerateMaskBitmap();
void resetBounds();
void copyFieldsFromBrush(const Brush& brush);
BrushType m_type; // Type of brush
int m_size; // Size (diameter)
@ -105,8 +118,6 @@ namespace doc {
std::optional<color_t> m_bgColor; // Background color
};
typedef std::shared_ptr<Brush> BrushRef;
} // namespace doc
#endif

View File

@ -1,4 +1,4 @@
-- Copyright (C) 2019-2023 Igara Studio S.A.
-- Copyright (C) 2019-2024 Igara Studio S.A.
--
-- This file is released under the terms of the MIT license.
-- Read LICENSE.txt for more information.
@ -51,6 +51,7 @@ do
assert(b.patternOrigin.y == 0)
end
-- Image brush
do
local rgba = app.pixelColor.rgba
local r = rgba(255, 0, 0)
@ -71,6 +72,15 @@ do
brush:setBgColor(b)
expect_img(brush.image, { b, g, g, b })
-- Test copy image brushes
local brush2 = Brush(brush)
expect_img(brush2.image, { b, g, g, b })
brush2:setFgColor(r)
expect_img(brush2.image, { b, r, r, b })
brush2:setBgColor(r)
expect_img(brush2.image, { r, r, r, r })
expect_img(brush.image, { b, g, g, b }) -- First brush wasn't modified
end
-- Tests with Image Brushes
@ -233,4 +243,4 @@ do
expect_img(cel.image,
{ pal:getColor(1).grayPixel, Color{ gray=222, alpha=222 }.grayPixel,
pal:getColor(3).grayPixel, Color{ gray=0, alpha=255 }.grayPixel })
end
end