Fix brush symmetry

Prior to this fix, asymmetric brushes weren't reflected, instead the brush was simply moved to a mirrored position.
This commit is contained in:
Gaspar Capello 2021-01-14 13:17:30 -03:00 committed by David Capello
parent 2be11cf2f5
commit 980454eac0
8 changed files with 170 additions and 187 deletions

View File

@ -588,7 +588,7 @@ add_library(app-lib
tools/pick_ink.cpp
tools/point_shape.cpp
tools/stroke.cpp
tools/symmetries.cpp
tools/symmetry.cpp
tools/tool_box.cpp
tools/tool_loop_manager.cpp
tools/velocity.cpp

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2020 Igara Studio S.A.
// Copyright (C) 2019-2021 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
@ -8,6 +8,7 @@
#include "app/util/wrap_point.h"
#include "app/tools/ink.h"
#include "doc/algorithm/flip_image.h"
#include "render/gradient.h"
namespace app {
@ -42,7 +43,7 @@ class BrushPointShape : public PointShape {
bool m_firstPoint;
Brush* m_lastBrush;
BrushType m_origBrushType;
std::shared_ptr<CompressedImage> m_compressedImage;
std::array<std::shared_ptr<CompressedImage>, 4> m_compressedImages;
// For dynamics
DynamicsOptions m_dynamics;
bool m_useDynamics;
@ -173,9 +174,7 @@ public:
// TODO cache compressed images (or remove them completelly)
if (m_lastBrush != brush) {
m_lastBrush = brush;
m_compressedImage.reset(new CompressedImage(brush->image(),
brush->maskBitmap(),
false));
m_compressedImages.fill(nullptr);
}
x += brush->bounds().x;
@ -208,7 +207,7 @@ public:
ink->prepareForPointShape(loop, m_firstPoint, x, y);
for (auto scanline : *m_compressedImage) {
for (auto scanline : getCompressedImage(pt.symmetry)) {
int u = x+scanline.x;
ink->prepareVForPointShape(loop, y+scanline.y);
doInkHline(u, y+scanline.y, u+scanline.w-1, loop);
@ -222,6 +221,47 @@ public:
area.y += y;
}
private:
CompressedImage& getCompressedImage(gen::SymmetryMode symmetryMode) {
auto& compressPtr = m_compressedImages[int(symmetryMode)];
if (!compressPtr) {
switch (symmetryMode) {
case gen::SymmetryMode::NONE: {
compressPtr.reset(new CompressedImage(m_lastBrush->image(),
m_lastBrush->maskBitmap(),
false));
break;
}
case gen::SymmetryMode::HORIZONTAL:
case gen::SymmetryMode::VERTICAL: {
std::unique_ptr<Image> tempImage(Image::createCopy(m_lastBrush->image()));
doc::algorithm::FlipType flip =
(symmetryMode == gen::SymmetryMode::HORIZONTAL)?
doc::algorithm::FlipType::FlipHorizontal:
doc::algorithm::FlipType::FlipVertical;
doc::algorithm::flip_image(tempImage.get(), tempImage->bounds(), flip);
compressPtr.reset(new CompressedImage(tempImage.get(),
m_lastBrush->maskBitmap(),
false));
break;
}
case gen::SymmetryMode::BOTH: {
std::unique_ptr<Image> tempImage(Image::createCopy(m_lastBrush->image()));
doc::algorithm::flip_image(tempImage.get(),
tempImage->bounds(),
doc::algorithm::FlipType::FlipVertical);
doc::algorithm::flip_image(tempImage.get(),
tempImage->bounds(),
doc::algorithm::FlipType::FlipHorizontal);
compressPtr.reset(new CompressedImage(tempImage.get(),
m_lastBrush->maskBitmap(),
false));
break;
}
}
}
return *compressPtr;
}
};
class FloodFillPointShape : public PointShape {

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2020 Igara Studio S.A.
// Copyright (C) 2019-2021 Igara Studio S.A.
// Copyright (C) 2001-2015 David Capello
//
// This program is distributed under the terms of
@ -9,6 +9,7 @@
#define APP_TOOLS_STROKE_H_INCLUDED
#pragma once
#include "app/pref/preferences.h"
#include "gfx/point.h"
#include "gfx/rect.h"
@ -25,6 +26,7 @@ namespace app {
float size = 0.0f;
float angle = 0.0f;
float gradient = 0.0f;
gen::SymmetryMode symmetry = gen::SymmetryMode::NONE;
Pt() { }
Pt(const gfx::Point& point) : x(point.x), y(point.y) { }
Pt(int x, int y) : x(x), y(y) { }

View File

@ -1,94 +0,0 @@
// Aseprite
// Copyright (C) 2020 Igara Studio S.A.
// Copyright (C) 2015-2017 David Capello
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "app/tools/symmetries.h"
#include "app/tools/point_shape.h"
#include "app/tools/stroke.h"
#include "app/tools/tool_loop.h"
#include "doc/brush.h"
namespace app {
namespace tools {
void HorizontalSymmetry::generateStrokes(const Stroke& mainStroke, Strokes& strokes,
ToolLoop* loop)
{
int brushSize, brushCenter;
if (loop->getPointShape()->isFloodFill()) {
brushSize = 1;
brushCenter = 0;
}
else {
// TODO we should flip the brush center+image+bitmap or just do
// the symmetry of all pixels
auto brush = loop->getBrush();
brushSize = brush->bounds().w;
brushCenter = brush->center().x;
}
strokes.push_back(mainStroke);
Stroke stroke2;
const bool isDynamic = loop->getDynamics().isDynamic();
for (const auto& pt : mainStroke) {
Stroke::Pt pt2 = pt;
if (isDynamic) {
brushSize = pt2.size;
brushCenter = (brushSize - brushSize % 2) / 2;
}
pt2.x = m_x - ((pt.x-brushCenter) - m_x + 1) - (brushSize - brushCenter - 1);
stroke2.addPoint(pt2);
}
strokes.push_back(stroke2);
}
void VerticalSymmetry::generateStrokes(const Stroke& mainStroke, Strokes& strokes,
ToolLoop* loop)
{
int brushSize, brushCenter;
if (loop->getPointShape()->isFloodFill()) {
brushSize = 1;
brushCenter = 0;
}
else {
auto brush = loop->getBrush();
brushSize = brush->bounds().h;
brushCenter = brush->center().y;
}
strokes.push_back(mainStroke);
Stroke stroke2;
const bool isDynamic = loop->getDynamics().isDynamic();
for (const auto& pt : mainStroke) {
Stroke::Pt pt2 = pt;
if (isDynamic) {
brushSize = pt2.size;
brushCenter = (brushSize - brushSize % 2) / 2;
}
pt2.y = m_y - ((pt.y-brushCenter) - m_y + 1) - (brushSize - brushCenter - 1);
stroke2.addPoint(pt2);
}
strokes.push_back(stroke2);
}
void SymmetryCombo::generateStrokes(const Stroke& mainStroke, Strokes& strokes,
ToolLoop* loop)
{
Strokes strokes0;
m_a->generateStrokes(mainStroke, strokes0, loop);
for (const Stroke& stroke : strokes0)
m_b->generateStrokes(stroke, strokes, loop);
}
} // namespace tools
} // namespace app

View File

@ -1,50 +0,0 @@
// Aseprite
// Copyright (C) 2015-2018 David Capello
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifndef APP_TOOLS_SYMMETRIES_H_INCLUDED
#define APP_TOOLS_SYMMETRIES_H_INCLUDED
#pragma once
#include "app/tools/stroke.h"
#include "app/tools/symmetry.h"
#include <memory>
namespace app {
namespace tools {
class HorizontalSymmetry : public Symmetry {
public:
HorizontalSymmetry(double x) : m_x(x) { }
void generateStrokes(const Stroke& mainStroke, Strokes& strokes,
ToolLoop* loop) override;
private:
double m_x;
};
class VerticalSymmetry : public Symmetry {
public:
VerticalSymmetry(double y) : m_y(y) { }
void generateStrokes(const Stroke& mainStroke, Strokes& strokes,
ToolLoop* loop) override;
private:
double m_y;
};
class SymmetryCombo : public Symmetry {
public:
SymmetryCombo(Symmetry* a, Symmetry* b) : m_a(a), m_b(b) { }
void generateStrokes(const Stroke& mainStroke, Strokes& strokes,
ToolLoop* loop) override;
private:
std::unique_ptr<tools::Symmetry> m_a;
std::unique_ptr<tools::Symmetry> m_b;
};
} // namespace tools
} // namespace app
#endif

View File

@ -0,0 +1,91 @@
// Aseprite
// Copyright (C) 2021 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "app/tools/symmetry.h"
#include "app/tools/point_shape.h"
#include "app/tools/tool_loop.h"
namespace app {
namespace tools {
void Symmetry::generateStrokes(const Stroke& stroke, Strokes& strokes,
ToolLoop* loop)
{
Stroke stroke2;
strokes.push_back(stroke);
gen::SymmetryMode symmetryMode = loop->getSymmetry()->mode();
switch (symmetryMode) {
case gen::SymmetryMode::NONE:
ASSERT(false);
break;
case gen::SymmetryMode::HORIZONTAL:
case gen::SymmetryMode::VERTICAL:
calculateSymmetricalStroke(stroke, stroke2, loop, symmetryMode);
strokes.push_back(stroke2);
break;
case gen::SymmetryMode::BOTH: {
calculateSymmetricalStroke(stroke, stroke2, loop, gen::SymmetryMode::HORIZONTAL);
strokes.push_back(stroke2);
Stroke stroke3;
calculateSymmetricalStroke(stroke, stroke3, loop, gen::SymmetryMode::VERTICAL);
strokes.push_back(stroke3);
Stroke stroke4;
calculateSymmetricalStroke(stroke3, stroke4, loop, gen::SymmetryMode::BOTH);
strokes.push_back(stroke4);
break;
}
}
}
void Symmetry::calculateSymmetricalStroke(const Stroke& refStroke, Stroke& stroke,
ToolLoop* loop, gen::SymmetryMode symmetryMode)
{
int brushSize, brushCenter;
if (loop->getPointShape()->isFloodFill()) {
brushSize = 1;
brushCenter = 0;
}
else {
// TODO we should flip the brush center+image+bitmap or just do
// the symmetry of all pixels
auto brush = loop->getBrush();
if (symmetryMode == gen::SymmetryMode::HORIZONTAL || symmetryMode == gen::SymmetryMode::BOTH) {
brushSize = brush->bounds().w;
brushCenter = brush->center().x;
}
else {
brushSize = brush->bounds().h;
brushCenter = brush->center().y;
}
}
const bool isDynamic = loop->getDynamics().isDynamic();
for (const auto& pt : refStroke) {
if (isDynamic) {
brushSize = pt.size;
brushCenter = (brushSize - brushSize % 2) / 2;
}
Stroke::Pt pt2 = pt;
pt2.symmetry = symmetryMode;
if (symmetryMode == gen::SymmetryMode::HORIZONTAL || symmetryMode == gen::SymmetryMode::BOTH)
pt2.x = 2 * (m_x + brushCenter) - pt2.x - brushSize;
else
pt2.y = 2 * (m_y + brushCenter) - pt2.y - brushSize;
stroke.addPoint(pt2);
}
}
} // namespace tools
} // namespace app

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2021 Igara Studio S.A.
// Copyright (C) 2015 David Capello
//
// This program is distributed under the terms of
@ -9,24 +10,34 @@
#pragma once
#include "app/tools/stroke.h"
#include <vector>
#include "app/pref/preferences.h"
namespace app {
namespace tools {
namespace tools {
class ToolLoop;
class ToolLoop;
// This class controls user input.
class Symmetry {
public:
virtual ~Symmetry() { }
class Symmetry {
public:
Symmetry(gen::SymmetryMode symmetryMode, double x, double y)
: m_symmetryMode(symmetryMode)
, m_x(x)
, m_y(y) {
}
// The "stroke" must be relative to the sprite origin.
virtual void generateStrokes(const Stroke& stroke, Strokes& strokes, ToolLoop* loop) = 0;
};
void generateStrokes(const Stroke& stroke, Strokes& strokes, ToolLoop* loop);
} // namespace tools
gen::SymmetryMode mode() const { return m_symmetryMode; }
private:
void calculateSymmetricalStroke(const Stroke& refStroke, Stroke& stroke,
ToolLoop* loop, gen::SymmetryMode symmetryMode);
gen::SymmetryMode m_symmetryMode;
double m_x, m_y;
};
} // namespace tools
} // namespace app
#endif

View File

@ -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
@ -29,7 +29,7 @@
#include "app/tools/freehand_algorithm.h"
#include "app/tools/ink.h"
#include "app/tools/point_shape.h"
#include "app/tools/symmetries.h"
#include "app/tools/symmetry.h"
#include "app/tools/tool.h"
#include "app/tools/tool_box.h"
#include "app/tools/tool_loop.h"
@ -204,27 +204,10 @@ public:
// Symmetry mode
if (Preferences::instance().symmetryMode.enabled()) {
switch (m_docPref.symmetry.mode()) {
case app::gen::SymmetryMode::NONE:
ASSERT(m_symmetry == nullptr);
break;
case app::gen::SymmetryMode::HORIZONTAL:
m_symmetry.reset(new app::tools::HorizontalSymmetry(m_docPref.symmetry.xAxis()));
break;
case app::gen::SymmetryMode::VERTICAL:
m_symmetry.reset(new app::tools::VerticalSymmetry(m_docPref.symmetry.yAxis()));
break;
case app::gen::SymmetryMode::BOTH:
m_symmetry.reset(
new app::tools::SymmetryCombo(
new app::tools::HorizontalSymmetry(m_docPref.symmetry.xAxis()),
new app::tools::VerticalSymmetry(m_docPref.symmetry.yAxis())));
break;
}
if (m_docPref.symmetry.mode() != gen::SymmetryMode::NONE)
m_symmetry.reset(new tools::Symmetry(m_docPref.symmetry.mode(),
m_docPref.symmetry.xAxis(),
m_docPref.symmetry.yAxis()));
}
// Ignore opacity for these inks