Add diagonal symmetry (fix #1171)

Added symmetry UI buttons, 45 and -45 degrees axis.
Now are possible activate all four symmetries, both
diagonal symmetries, individual diagonal symmetry, and
the old horizontal/vertical combinations.

Also added symmetry to image brushes (prior to this fix:
only the brush position was symmetrical, the image brush
was not flipped).
This commit is contained in:
Gaspar Capello 2024-11-01 15:05:23 -03:00
parent 053a538272
commit b7aa5d5ebe
22 changed files with 405 additions and 154 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -367,9 +367,9 @@
<part id="icon_white" x="64" y="256" w="16" h="16" />
<part id="icon_transparent" x="80" y="256" w="16" h="16" />
<part id="color_wheel_indicator" x="48" y="192" w="4" h="4" />
<part id="no_symmetry" x="144" y="240" w="13" h="13" />
<part id="horizontal_symmetry" x="160" y="240" w="13" h="13" />
<part id="vertical_symmetry" x="176" y="240" w="13" h="13" />
<part id="no_symmetry" x="128" y="240" w="13" h="13" />
<part id="horizontal_symmetry" x="144" y="240" w="13" h="13" />
<part id="vertical_symmetry" x="160" y="240" w="13" h="13" />
<part id="icon_arrow_down" x="144" y="256" w="7" h="4" />
<part id="icon_close" x="152" y="256" w="7" h="7" />
<part id="icon_search" x="160" y="256" w="8" h="8" />
@ -454,6 +454,8 @@
<part id="multi_win_icon" x="104" y="256" w="8" h="7" />
<part id="spin_up" x="128" y="256" w="5" h="3" />
<part id="spin_down" x="128" y="259" w="5" h="3" />
<part id="right_diagonal_symmetry" x="176" y="240" w="13" h="13" />
<part id="left_diagonal_symmetry" x="192" y="240" w="13" h="13" />
</parts>
<styles>
<style id="box" />

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -363,9 +363,9 @@
<part id="icon_white" x="64" y="256" w="16" h="16" />
<part id="icon_transparent" x="80" y="256" w="16" h="16" />
<part id="color_wheel_indicator" x="48" y="192" w="4" h="4" />
<part id="no_symmetry" x="144" y="240" w="13" h="13" />
<part id="horizontal_symmetry" x="160" y="240" w="13" h="13" />
<part id="vertical_symmetry" x="176" y="240" w="13" h="13" />
<part id="no_symmetry" x="128" y="240" w="13" h="13" />
<part id="horizontal_symmetry" x="144" y="240" w="13" h="13" />
<part id="vertical_symmetry" x="160" y="240" w="13" h="13" />
<part id="icon_arrow_down" x="144" y="256" w="7" h="4" />
<part id="icon_close" x="152" y="256" w="7" h="7" />
<part id="icon_search" x="160" y="256" w="8" h="8" />
@ -450,6 +450,8 @@
<part id="multi_win_icon" x="104" y="256" w="8" h="7" />
<part id="spin_up" x="128" y="256" w="5" h="3" />
<part id="spin_down" x="128" y="259" w="5" h="3" />
<part id="right_diagonal_symmetry" x="176" y="240" w="13" h="13" />
<part id="left_diagonal_symmetry" x="192" y="240" w="13" h="13" />
</parts>
<styles>
<style id="box" />

View File

@ -548,6 +548,12 @@
<key command="SymmetryMode">
<param name="orientation" value="horizontal" />
</key>
<key command="SymmetryMode">
<param name="orientation" value="right_diagonal" />
</key>
<key command="SymmetryMode">
<param name="orientation" value="left_diagonal" />
</key>
<key command="AutocropSprite" />
<key command="AutocropSprite">
<param name="byGrid" value="true" />

View File

@ -94,6 +94,10 @@
<value id="HORIZONTAL" value="1" />
<value id="VERTICAL" value="2" />
<value id="BOTH" value="3" />
<value id="RIGHT_DIAG" value="4" />
<value id="LEFT_DIAG" value="8" />
<value id="BOTH_DIAG" value="12" />
<value id="ALL" value="15" />
</enum>
<enum id="PaintingCursorType">
<value id="SIMPLE_CROSSHAIR" value="0" />

View File

@ -1881,6 +1881,8 @@ image_preset_text = Text
toggle = Toggle Symmetry
toggle_horizontal = Toggle Horizontal Symmetry
toggle_vertical = Toggle Vertical Symmetry
toggle_right_diagonal = Toggle 45° Symmetry
toggle_left_diagonal = Toggle -45° Symmetry
show_options = Symmetry Options
reset_position = Reset Symmetry to Center
reset_position_to_view_center = Reset Symmetry to View Center

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018 Igara Studio S.A.
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
@ -49,6 +49,10 @@ std::string SymmetryModeCommand::onGetFriendlyName() const
return Strings::symmetry_toggle_horizontal();
case app::gen::SymmetryMode::VERTICAL:
return Strings::symmetry_toggle_vertical();
case app::gen::SymmetryMode::RIGHT_DIAG:
return Strings::symmetry_toggle_right_diagonal();
case app::gen::SymmetryMode::LEFT_DIAG:
return Strings::symmetry_toggle_left_diagonal();
default:
return Strings::symmetry_toggle();
}
@ -59,6 +63,8 @@ void SymmetryModeCommand::onLoadParams(const Params& params)
std::string mode = params.get("orientation");
if (mode == "vertical") m_mode = app::gen::SymmetryMode::VERTICAL;
else if (mode == "horizontal") m_mode = app::gen::SymmetryMode::HORIZONTAL;
else if (mode == "right_diagonal") m_mode = app::gen::SymmetryMode::RIGHT_DIAG;
else if (mode == "left_diagonal") m_mode = app::gen::SymmetryMode::LEFT_DIAG;
else m_mode = app::gen::SymmetryMode::NONE;
}

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2022 Igara Studio S.A.
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
@ -104,7 +104,8 @@ namespace app {
virtual void prepareForStrokes(ToolLoop* loop, Strokes& strokes) { }
// Called for each point shape.
virtual void prepareForPointShape(ToolLoop* loop, bool firstPoint, int x, int y) { }
virtual void prepareForPointShape(ToolLoop* loop, bool firstPoint, int x, int y,
doc::SymmetryIndex symmetry) { }
virtual void prepareVForPointShape(ToolLoop* loop, int y) { }
virtual void prepareUForPointShapeWholeScanline(ToolLoop* loop, int x1) { }
virtual void prepareUForPointShapeSlicedScanline(ToolLoop* loop, bool leftSlice, int x1) { }

View File

@ -6,6 +6,7 @@
// the End-User License Agreement for Aseprite.
#include "app/color_utils.h"
#include "app/tools/symmetry.h"
#include "app/util/wrap_point.h"
#include "app/util/wrap_value.h"
#include "doc/blend_funcs.h"
@ -33,7 +34,8 @@ public:
virtual ~BaseInkProcessing() { }
virtual void processScanline(int x1, int y, int x2, ToolLoop* loop) = 0;
virtual void prepareForStrokes(ToolLoop* loop, Strokes& strokes) { }
virtual void prepareForPointShape(ToolLoop* loop, bool firstPoint, int x, int y) { }
virtual void prepareForPointShape(ToolLoop* loop, bool firstPoint, int x, int y,
doc::SymmetryIndex index) { }
virtual void prepareVForPointShape(ToolLoop* loop, int y) { }
virtual void prepareUForPointShapeWholeScanline(ToolLoop* loop, int x1) { }
virtual void prepareUForPointShapeSlicedScanline(ToolLoop* loop, bool leftSlice, int x1) { }
@ -136,7 +138,8 @@ public:
CopyInkProcessing(ToolLoop* loop) {
}
void prepareForPointShape(ToolLoop* loop, bool firstPoint, int x, int y) override {
void prepareForPointShape(ToolLoop* loop, bool firstPoint, int x, int y,
doc::SymmetryIndex index) override {
m_color = loop->getPrimaryColor();
if (loop->getLayer()->isBackground()) {
@ -166,7 +169,8 @@ public:
: m_opacity(loop->getOpacity()) {
}
void prepareForPointShape(ToolLoop* loop, bool firstPoint, int x, int y) override {
void prepareForPointShape(ToolLoop* loop, bool firstPoint, int x, int y,
doc::SymmetryIndex index) override {
m_color = loop->getPrimaryColor();
}
@ -207,7 +211,8 @@ public:
, m_maskIndex(loop->getLayer()->isBackground() ? -1: loop->sprite()->transparentColor()) {
}
void prepareForPointShape(ToolLoop* loop, bool firstPoint, int x, int y) override {
void prepareForPointShape(ToolLoop* loop, bool firstPoint, int x, int y,
doc::SymmetryIndex index) override {
m_color = m_palette->getEntry(loop->getPrimaryColor());
}
@ -246,7 +251,8 @@ public:
m_opacity = loop->getOpacity();
}
void prepareForPointShape(ToolLoop* loop, bool firstPoint, int x, int y) override {
void prepareForPointShape(ToolLoop* loop, bool firstPoint, int x, int y,
doc::SymmetryIndex index) override {
m_color = loop->getPrimaryColor();
}
@ -280,7 +286,8 @@ public:
m_colorIndex(loop->getFgColor()) {
}
void prepareForPointShape(ToolLoop* loop, bool firstPoint, int x, int y) override {
void prepareForPointShape(ToolLoop* loop, bool firstPoint, int x, int y,
doc::SymmetryIndex index) override {
m_color = m_palette->getEntry(loop->getPrimaryColor());
}
@ -318,7 +325,8 @@ public:
m_opacity = loop->getOpacity();
}
void prepareForPointShape(ToolLoop* loop, bool firstPoint, int x, int y) override {
void prepareForPointShape(ToolLoop* loop, bool firstPoint, int x, int y,
doc::SymmetryIndex index) override {
m_color = loop->getPrimaryColor();
}
@ -351,7 +359,8 @@ public:
m_maskIndex(loop->getLayer()->isBackground() ? -1: loop->sprite()->transparentColor()) {
}
void prepareForPointShape(ToolLoop* loop, bool firstPoint, int x, int y) override {
void prepareForPointShape(ToolLoop* loop, bool firstPoint, int x, int y,
doc::SymmetryIndex index) override {
m_color = (int(loop->getPrimaryColor()) == m_maskIndex ?
(m_palette->getEntry(loop->getPrimaryColor()) & rgba_rgb_mask):
(m_palette->getEntry(loop->getPrimaryColor())));
@ -1129,10 +1138,6 @@ public:
m_bgColor = loop->getSecondaryColor();
m_palette = loop->getPalette();
m_brush = loop->getBrush();
m_brushImage = (m_brush->patternImage() ? m_brush->patternImage():
m_brush->image());
m_brushMask = m_brush->maskBitmap();
m_patternAlign = m_brush->pattern();
m_opacity = loop->getOpacity();
m_width = m_brush->bounds().w;
m_height = m_brush->bounds().h;
@ -1147,14 +1152,22 @@ public:
}
else
m_transparentColor = 0;
m_isBoundsRotated = false;
}
void prepareForPointShape(ToolLoop* loop, bool firstPoint, int x, int y) override {
void prepareForPointShape(ToolLoop* loop, bool firstPoint, int x, int y,
doc::SymmetryIndex index) override {
m_isBoundsRotated = does_symmetry_rotate_image(index);
m_brushImage = m_brush->getSymmetryImage(index);
m_brushMask = m_brush->getSymmetryMask(index);
m_patternAlign = m_brush->pattern();
if (m_patternAlign != BrushPattern::ALIGNED_TO_SRC) {
const int brushW = brushWidth();
const int brushH = brushHeight();
// Case: during painting process with PaintBucket Tool
if (loop->getPointShape()->isFloodFill()) {
m_u = x - m_brush->bounds().w / 2;
m_v = y - m_brush->bounds().h / 2;
m_u = x - brushW / 2;
m_v = y - brushH / 2;
}
// Case: during brush preview of PaintBucket Tool
else if (loop->getController()->isOnePoint()) {
@ -1162,51 +1175,54 @@ public:
m_v = 0;
}
else {
m_u = ((m_brush->patternOrigin().x % loop->sprite()->width()) - loop->getCelOrigin().x) % m_width;
m_v = ((m_brush->patternOrigin().y % loop->sprite()->height()) - loop->getCelOrigin().y) % m_height;
m_u = ((m_brush->patternOrigin().x % loop->sprite()->width()) - loop->getCelOrigin().x) % brushW;
m_v = ((m_brush->patternOrigin().y % loop->sprite()->height()) - loop->getCelOrigin().y) % brushH;
}
}
}
void prepareVForPointShape(ToolLoop* loop, int y) override {
const int brushH = brushHeight();
if (m_patternAlign == doc::BrushPattern::ALIGNED_TO_SRC) {
m_v = (m_brush->patternOrigin().y - loop->getCelOrigin().y) % m_height;
if (m_v < 0) m_v += m_height;
m_v = (m_brush->patternOrigin().y - loop->getCelOrigin().y) % brushH;
if (m_v < 0) m_v += brushH;
}
else {
int spriteH = loop->sprite()->height();
if (y/spriteH > 0)
// 'y' is outside of the center tile.
m_v = (m_brush->patternOrigin().y + m_height - (y/spriteH) * spriteH) % m_height;
m_v = (m_brush->patternOrigin().y + brushH - (y/spriteH) * spriteH) % brushH;
else
// 'y' is inside of the center tile.
m_v = ((m_brush->patternOrigin().y % spriteH) - loop->getCelOrigin().y) % m_height;
m_v = ((m_brush->patternOrigin().y % spriteH) - loop->getCelOrigin().y) % brushH;
}
}
void prepareUForPointShapeWholeScanline(ToolLoop* loop, int x1) override {
const int brushW = brushWidth();
if (m_patternAlign == doc::BrushPattern::ALIGNED_TO_SRC) {
m_u = (m_brush->patternOrigin().x - loop->getCelOrigin().x) % m_width;
if (m_u < 0) m_u += m_height;
m_u = (m_brush->patternOrigin().x - loop->getCelOrigin().x) % brushW;
if (m_u < 0) m_u += brushHeight();
}
else {
m_u = ((m_brush->patternOrigin().x % loop->sprite()->width()) - loop->getCelOrigin().x ) % m_width;
m_u = ((m_brush->patternOrigin().x % loop->sprite()->width()) - loop->getCelOrigin().x ) % brushW;
if (x1/loop->sprite()->width() > 0)
m_u = (m_brush->patternOrigin().x + m_width - (x1/loop->sprite()->width()) * loop->sprite()->width()) % m_width;
m_u = (m_brush->patternOrigin().x + brushW - (x1/loop->sprite()->width()) * loop->sprite()->width()) % brushW;
}
}
void prepareUForPointShapeSlicedScanline(ToolLoop* loop, bool leftSlice, int x1) override {
const int brushW(brushWidth());
if (m_patternAlign == doc::BrushPattern::ALIGNED_TO_SRC) {
m_u = (m_brush->patternOrigin().x - loop->getCelOrigin().x) % m_width;
if (m_u < 0) m_u += m_height;
m_u = (m_brush->patternOrigin().x - loop->getCelOrigin().x) % brushW;
if (m_u < 0) m_u += brushHeight();
return;
}
else {
if (leftSlice)
m_u = ((m_brush->patternOrigin().x % loop->sprite()->width()) - loop->getCelOrigin().x ) % m_width;
m_u = ((m_brush->patternOrigin().x % loop->sprite()->width()) - loop->getCelOrigin().x ) % brushW;
else
m_u = (m_brush->patternOrigin().x + m_width - (x1/loop->sprite()->width() + 1) * loop->sprite()->width()) % m_width;
m_u = (m_brush->patternOrigin().x + brushW - (x1/loop->sprite()->width() + 1) * loop->sprite()->width()) % brushW;
}
}
@ -1224,12 +1240,16 @@ public:
protected:
bool alignPixelPoint(int& x0, int& y0) {
int x = (x0 - m_u) % m_width;
int y = (y0 - m_v) % m_height;
if (x < 0) x = m_width - ((-x) % m_width);
if (y < 0) y = m_height - ((-y) % m_height);
if (m_brushMask && !get_pixel_fast<BitmapTraits>(m_brushMask, x, y))
const int brushW = brushWidth();
const int brushH = brushHeight();
int x = (x0 - m_u) % brushW;
int y = (y0 - m_v) % brushH;
if (x < 0) x = brushW - ((-x) % brushW);
if (y < 0) y = brushH - ((-y) % brushH);
if (m_brushMask &&
!get_pixel_fast<BitmapTraits>(m_brushMask, x, y))
return false;
if (m_brush->patternImage()) {
@ -1246,10 +1266,17 @@ protected:
return true;
}
inline int brushWidth() const {
return m_isBoundsRotated ? m_height : m_width;
}
inline int brushHeight() const {
return m_isBoundsRotated ? m_width : m_height;
}
color_t m_fgColor;
color_t m_bgColor;
const Palette* m_palette;
const Brush* m_brush;
Brush* m_brush;
const Image* m_brushImage;
const Image* m_brushMask;
BrushPattern m_patternAlign;
@ -1259,6 +1286,7 @@ protected:
// which is the background color in order to translate to transparent color
// in a RGBA sprite.
color_t m_transparentColor;
bool m_isBoundsRotated;
};
template<>

View File

@ -31,9 +31,10 @@ public:
m_proc->processScanline(x1, y, x2, loop);
}
void prepareForPointShape(ToolLoop* loop, bool firstPoint, int x, int y) override {
void prepareForPointShape(ToolLoop* loop, bool firstPoint, int x, int y,
doc::SymmetryIndex symmetry) override {
ASSERT(m_proc);
m_proc->prepareForPointShape(loop, firstPoint, x, y);
m_proc->prepareForPointShape(loop, firstPoint, x, y, symmetry);
}
void prepareVForPointShape(ToolLoop* loop, int y) override {

View File

@ -617,7 +617,7 @@ private:
// IntertwineAsPixelPerfect.joinStroke() method.
void savePointshapeStrokePtArea(ToolLoop* loop, const tools::Stroke::Pt& pt) {
gfx::Rect r;
loop->getPointShape()->getModifiedArea(loop, pt.x, pt.y, r);
loop->getPointShape()->getModifiedArea(loop, pt.x, pt.y, pt.symmetry, r);
gfx::Region rgn(r);
// By wrapping the modified area's position when tiled mode is active, the
@ -689,7 +689,7 @@ private:
ASSERT(m_tempTileset);
gfx::Rect r;
loop->getPointShape()->getModifiedArea(loop, pt.x, pt.y, r);
loop->getPointShape()->getModifiedArea(loop, pt.x, pt.y, pt.symmetry, r);
r.offset(-loop->getCelOrigin());
auto tilesPts = m_dstGrid.tilesInCanvasRegion(gfx::Region(r));

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2020 Igara Studio S.A.
// Copyright (C) 2020-2024 Igara Studio S.A.
// Copyright (C) 2001-2016 David Capello
//
// This program is distributed under the terms of
@ -28,7 +28,8 @@ namespace app {
// The x, y position must be relative to the cel/src/dst image origin.
virtual void transformPoint(ToolLoop* loop, const Stroke::Pt& pt) = 0;
virtual void getModifiedArea(ToolLoop* loop, int x, int y, gfx::Rect& area) = 0;
virtual void getModifiedArea(ToolLoop* loop, int x, int y,
doc::SymmetryIndex symmetry, gfx::Rect& area) = 0;
protected:
// Calls loop->getInk()->inkHline() function for each horizontal-scanline

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2022 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
@ -8,7 +8,9 @@
#include "app/util/wrap_point.h"
#include "app/tools/ink.h"
#include "app/tools/symmetry.h"
#include "doc/algorithm/flip_image.h"
#include "doc/primitives.h"
#include "render/gradient.h"
#include <array>
@ -23,7 +25,8 @@ public:
// Do nothing
}
void getModifiedArea(ToolLoop* loop, int x, int y, Rect& area) override {
void getModifiedArea(ToolLoop* loop, int x, int y,
doc::SymmetryIndex symmetry, Rect& area) override {
// Do nothing
}
};
@ -33,11 +36,12 @@ public:
bool isPixel() override { return true; }
void transformPoint(ToolLoop* loop, const Stroke::Pt& pt) override {
loop->getInk()->prepareForPointShape(loop, true, pt.x, pt.y);
loop->getInk()->prepareForPointShape(loop, true, pt.x, pt.y, pt.symmetry);
doInkHline(pt.x, pt.y, pt.x, loop);
}
void getModifiedArea(ToolLoop* loop, int x, int y, Rect& area) override {
void getModifiedArea(ToolLoop* loop, int x, int y,
doc::SymmetryIndex symmetry, Rect& area) override {
area = Rect(x, y, 1, 1);
}
};
@ -51,11 +55,12 @@ public:
const doc::Grid& grid = loop->getGrid();
gfx::Point newPos = grid.canvasToTile(pt.toPoint());
loop->getInk()->prepareForPointShape(loop, true, newPos.x, newPos.y);
loop->getInk()->prepareForPointShape(loop, true, newPos.x, newPos.y, pt.symmetry);
doInkHline(newPos.x, newPos.y, newPos.x, loop);
}
void getModifiedArea(ToolLoop* loop, int x, int y, Rect& area) override {
void getModifiedArea(ToolLoop* loop, int x, int y,
doc::SymmetryIndex symmetry, Rect& area) override {
const doc::Grid& grid = loop->getGrid();
area = grid.alignBounds(Rect(x, y, 1, 1));
}
@ -65,7 +70,7 @@ class BrushPointShape : public PointShape {
bool m_firstPoint;
Brush* m_lastBrush;
BrushType m_origBrushType;
std::array<std::shared_ptr<CompressedImage>, 4> m_compressedImages;
std::array<std::shared_ptr<CompressedImage>, int(SymmetryIndex::ELEMENTS)> m_compressedImages;
// For dynamics
DynamicsOptions m_dynamics;
bool m_useDynamics;
@ -213,8 +218,15 @@ public:
m_compressedImages.fill(nullptr);
}
x += brush->bounds().x;
y += brush->bounds().y;
if (brush->type() == kImageBrushType &&
does_symmetry_rotate_image(pt.symmetry)) {
x += brush->bounds().y;
y += brush->bounds().x;
}
else {
x += brush->bounds().x;
y += brush->bounds().y;
}
if (m_firstPoint) {
if ((brush->type() == kImageBrushType) &&
@ -241,7 +253,7 @@ public:
y = wrap_value(y, loop->sprite()->height());
}
ink->prepareForPointShape(loop, m_firstPoint, x, y);
ink->prepareForPointShape(loop, m_firstPoint, x, y, pt.symmetry);
for (auto scanline : getCompressedImage(pt.symmetry)) {
int u = x+scanline.x;
@ -251,50 +263,24 @@ public:
m_firstPoint = false;
}
void getModifiedArea(ToolLoop* loop, int x, int y, Rect& area) override {
area = loop->getBrush()->bounds();
void getModifiedArea(ToolLoop* loop, int x, int y,
doc::SymmetryIndex symmetry, Rect& area) override {
auto bounds = loop->getBrush()->bounds();
if (does_symmetry_rotate_image(symmetry))
area = gfx::Rect(bounds.y, bounds.x, bounds.h, bounds.w);
else
area = bounds;
area.x += x;
area.y += y;
}
private:
CompressedImage& getCompressedImage(gen::SymmetryMode symmetryMode) {
auto& compressPtr = m_compressedImages[int(symmetryMode)];
CompressedImage& getCompressedImage(doc::SymmetryIndex index) {
auto& compressPtr = m_compressedImages[int(index)];
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;
}
}
compressPtr.reset(new CompressedImage(m_lastBrush->getSymmetryImage(index),
m_lastBrush->getSymmetryMask(index),
false));
}
return *compressPtr;
}
@ -319,7 +305,7 @@ public:
wpt, true);
}
loop->getInk()->prepareForPointShape(loop, true, wpt.x, wpt.y);
loop->getInk()->prepareForPointShape(loop, true, wpt.x, wpt.y, pt.symmetry);
doc::algorithm::floodfill(
srcImage,
@ -334,7 +320,8 @@ public:
loop, (AlgoHLine)doInkHline);
}
void getModifiedArea(ToolLoop* loop, int x, int y, Rect& area) override {
void getModifiedArea(ToolLoop* loop, int x, int y,
doc::SymmetryIndex symmetry, Rect& area) override {
area = floodfillBounds(loop, x, y);
}
@ -387,7 +374,7 @@ public:
}
void transformPoint(ToolLoop* loop, const Stroke::Pt& pt) override {
loop->getInk()->prepareForPointShape(loop, true, pt.x, pt.y);
loop->getInk()->prepareForPointShape(loop, true, pt.x, pt.y, pt.symmetry);
int spray_width = loop->getSprayWidth();
int spray_speed = loop->getSpraySpeed();
@ -418,15 +405,16 @@ public:
}
}
void getModifiedArea(ToolLoop* loop, int x, int y, Rect& area) override {
void getModifiedArea(ToolLoop* loop, int x, int y,
doc::SymmetryIndex symmetry, Rect& area) override {
int spray_width = loop->getSprayWidth();
Point p1(x-spray_width, y-spray_width);
Point p2(x+spray_width, y+spray_width);
Rect area1;
Rect area2;
m_subPointShape.getModifiedArea(loop, p1.x, p1.y, area1);
m_subPointShape.getModifiedArea(loop, p2.x, p2.y, area2);
m_subPointShape.getModifiedArea(loop, p1.x, p1.y, symmetry, area1);
m_subPointShape.getModifiedArea(loop, p2.x, p2.y, symmetry, area2);
area = area1.createUnion(area2);
}

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-2015 David Capello
//
// This program is distributed under the terms of
@ -10,6 +10,7 @@
#pragma once
#include "app/pref/preferences.h"
#include "doc/brush.h"
#include "gfx/point.h"
#include "gfx/rect.h"
@ -26,7 +27,7 @@ namespace app {
float size = 0.0f;
float angle = 0.0f;
float gradient = 0.0f;
gen::SymmetryMode symmetry = gen::SymmetryMode::NONE;
doc::SymmetryIndex symmetry = doc::SymmetryIndex::ORIGINAL;
Pt() { }
Pt(const gfx::Point& point) : x(point.x), y(point.y) { }
Pt(int x, int y) : x(x), y(y) { }

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2021 Igara Studio S.A.
// Copyright (C) 2021-2024 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -12,6 +12,7 @@
#include "app/tools/point_shape.h"
#include "app/tools/tool_loop.h"
#include "doc/brush.h"
namespace app {
namespace tools {
@ -28,61 +29,147 @@ void Symmetry::generateStrokes(const Stroke& stroke, Strokes& strokes,
break;
case gen::SymmetryMode::HORIZONTAL:
calculateSymmetricalStroke(stroke, stroke2, loop, doc::SymmetryIndex::FLIPPED_X);
strokes.push_back(stroke2);
break;
case gen::SymmetryMode::VERTICAL:
calculateSymmetricalStroke(stroke, stroke2, loop, symmetryMode);
calculateSymmetricalStroke(stroke, stroke2, loop, doc::SymmetryIndex::FLIPPED_Y);
strokes.push_back(stroke2);
break;
case gen::SymmetryMode::BOTH: {
calculateSymmetricalStroke(stroke, stroke2, loop, gen::SymmetryMode::HORIZONTAL);
calculateSymmetricalStroke(stroke, stroke2, loop, doc::SymmetryIndex::FLIPPED_X);
strokes.push_back(stroke2);
Stroke stroke3;
calculateSymmetricalStroke(stroke, stroke3, loop, gen::SymmetryMode::VERTICAL);
calculateSymmetricalStroke(stroke, stroke3, loop, doc::SymmetryIndex::FLIPPED_Y);
strokes.push_back(stroke3);
Stroke stroke4;
calculateSymmetricalStroke(stroke3, stroke4, loop, gen::SymmetryMode::BOTH);
calculateSymmetricalStroke(stroke3, stroke4, loop, doc::SymmetryIndex::FLIPPED_XY);
strokes.push_back(stroke4);
break;
}
case gen::SymmetryMode::RIGHT_DIAG: {
calculateSymmetricalStroke(stroke, stroke2, loop, doc::SymmetryIndex::ROT_FLIP_270);
strokes.push_back(stroke2);
break;
}
case gen::SymmetryMode::LEFT_DIAG: {
calculateSymmetricalStroke(stroke, stroke2, loop, doc::SymmetryIndex::ROT_FLIP_90);
strokes.push_back(stroke2);
break;
}
case gen::SymmetryMode::BOTH_DIAG: {
calculateSymmetricalStroke(stroke, stroke2, loop, doc::SymmetryIndex::ROT_FLIP_270);
strokes.push_back(stroke2);
Stroke stroke3;
calculateSymmetricalStroke(stroke, stroke3, loop, doc::SymmetryIndex::ROT_FLIP_90);
strokes.push_back(stroke3);
Stroke stroke4;
calculateSymmetricalStroke(stroke, stroke4, loop, doc::SymmetryIndex::FLIPPED_XY, true);
strokes.push_back(stroke4);
break;
}
case gen::SymmetryMode::ALL: {
calculateSymmetricalStroke(stroke, stroke2, loop, doc::SymmetryIndex::FLIPPED_X);
strokes.push_back(stroke2);
Stroke stroke3;
calculateSymmetricalStroke(stroke, stroke3, loop, doc::SymmetryIndex::FLIPPED_Y);
strokes.push_back(stroke3);
Stroke stroke4;
calculateSymmetricalStroke(stroke3, stroke4, loop, doc::SymmetryIndex::FLIPPED_XY);
strokes.push_back(stroke4);
Stroke stroke5;
calculateSymmetricalStroke(stroke, stroke5, loop, doc::SymmetryIndex::ROT_FLIP_90);
strokes.push_back(stroke5);
Stroke stroke6;
calculateSymmetricalStroke(stroke5, stroke6, loop, doc::SymmetryIndex::ROTATED_270);
strokes.push_back(stroke6);
Stroke stroke7;
calculateSymmetricalStroke(stroke, stroke7, loop, doc::SymmetryIndex::ROT_FLIP_270);
strokes.push_back(stroke7);
Stroke stroke8;
calculateSymmetricalStroke(stroke7, stroke8, loop, doc::SymmetryIndex::ROTATED_90);
strokes.push_back(stroke8);
break;
}
}
}
void Symmetry::calculateSymmetricalStroke(const Stroke& refStroke, Stroke& stroke,
ToolLoop* loop, gen::SymmetryMode symmetryMode)
void Symmetry::calculateSymmetricalStroke(const Stroke& refStroke,
Stroke& stroke,
ToolLoop* loop,
const doc::SymmetryIndex symmetry,
const bool isDoubleDiagonalSymmetry)
{
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;
gfx::Size brushSize(1, 1);
gfx::Point brushCenter(0, 0);
auto brush = loop->getBrush();
if (!loop->getPointShape()->isFloodFill()) {
if (!does_symmetry_rotate_image(symmetry)) {
brushSize = brush->bounds().size();
brushCenter = brush->center();
}
else {
brushSize = brush->bounds().h;
brushCenter = brush->center().y;
brushSize = gfx::Size(brush->bounds().h,
brush->bounds().w);
brushCenter = gfx::Point(brush->center().y,
brush->center().x);
}
}
const bool isDynamic = loop->getDynamics().isDynamic();
for (const auto& pt : refStroke) {
if (isDynamic) {
brushSize = pt.size;
brushCenter = (brushSize - brushSize % 2) / 2;
brushSize = gfx::Size(pt.size, pt.size);
int center = (brushSize.w - brushSize.w % 2) / 2;
brushCenter = gfx::Point(center, center);
}
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;
pt2.symmetry = symmetry;
switch (symmetry) {
case doc::SymmetryIndex::ROT_FLIP_270: {
int adj_x = 0;
int adj_y = 0;
if (m_x - double(int(m_x)) > 0) adj_y = 1;
if (m_y - double(int(m_y)) > 0) adj_x = 1;
if (adj_x == 1 && adj_y == 1) { adj_x = 0; adj_y = 0; }
pt2.x = -pt.y + m_x + m_y - (brushSize.w % 2 ? 1 : 0) + adj_x;
pt2.y = -pt.x + m_x + m_y - (brushSize.h % 2 ? 1 : 0) + adj_y;
break;
}
case doc::SymmetryIndex::ROT_FLIP_90:
pt2.x = pt.y + m_x - m_y + (m_x - int(m_x));
pt2.y = pt.x - m_x + m_y + (m_y - int(m_y));
break;
case doc::SymmetryIndex::ROTATED_90:
case doc::SymmetryIndex::ROTATED_270:
pt2.y = 2 * m_y - pt.y - (brushSize.h % 2 ? 1 : 0);
break;
case doc::SymmetryIndex::FLIPPED_X:
case doc::SymmetryIndex::FLIPPED_XY: {
pt2.x = 2 * (m_x + brushCenter.x) - pt.x - brushSize.w;
if (isDoubleDiagonalSymmetry)
pt2.y = 2 * (m_y + brushCenter.y) - pt.y - brushSize.h;
break;
}
default:
pt2.y = 2 * (m_y + brushCenter.y) - pt.y - brushSize.h;
break;
}
stroke.addPoint(pt2);
}
}

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2021 Igara Studio S.A.
// Copyright (C) 2021-2024 Igara Studio S.A.
// Copyright (C) 2015 David Capello
//
// This program is distributed under the terms of
@ -11,12 +11,20 @@
#include "app/tools/stroke.h"
#include "app/pref/preferences.h"
#include "doc/brush.h"
namespace app {
namespace tools {
class ToolLoop;
static inline bool does_symmetry_rotate_image(doc::SymmetryIndex symmetry) {
return symmetry == doc::SymmetryIndex::ROTATED_90 ||
symmetry == doc::SymmetryIndex::ROTATED_270 ||
symmetry == doc::SymmetryIndex::ROT_FLIP_90 ||
symmetry == doc::SymmetryIndex::ROT_FLIP_270;
}
class Symmetry {
public:
Symmetry(gen::SymmetryMode symmetryMode, double x, double y)
@ -30,8 +38,11 @@ public:
gen::SymmetryMode mode() const { return m_symmetryMode; }
private:
void calculateSymmetricalStroke(const Stroke& refStroke, Stroke& stroke,
ToolLoop* loop, gen::SymmetryMode symmetryMode);
void calculateSymmetricalStroke(const Stroke& refStroke,
Stroke& stroke,
ToolLoop* loop,
const doc::SymmetryIndex symmetry,
const bool isDoubleDiagonalSymmetry = false);
gen::SymmetryMode m_symmetryMode;
double m_x, m_y;

View File

@ -44,8 +44,7 @@ using namespace filters;
ToolLoopManager::ToolLoopManager(ToolLoop* toolLoop)
: m_toolLoop(toolLoop)
, m_canceled(false)
, m_brushSize0(toolLoop->getBrush()->size())
, m_brushAngle0(toolLoop->getBrush()->angle())
, m_brush0(toolLoop->getBrush())
, m_dynamics(toolLoop->getDynamics())
{
}
@ -209,7 +208,7 @@ void ToolLoopManager::movement(Pointer pointer)
doLoopStep(false);
}
void ToolLoopManager::disableMouseStabilizer()
void ToolLoopManager::disableMouseStabilizer()
{
// Disable mouse stabilizer for the current ToolLoopManager
m_dynamics.stabilizer = false;
@ -341,12 +340,14 @@ void ToolLoopManager::calculateDirtyArea(const Strokes& strokes)
m_toolLoop->getPointShape()->getModifiedArea(
m_toolLoop,
strokeBounds.x,
strokeBounds.y, r1);
strokeBounds.y,
stroke.firstPoint().symmetry, r1);
m_toolLoop->getPointShape()->getModifiedArea(
m_toolLoop,
strokeBounds.x+strokeBounds.w-1,
strokeBounds.y+strokeBounds.h-1, r2);
strokeBounds.y+strokeBounds.h-1,
stroke.firstPoint().symmetry, r2);
m_dirtyArea.createUnion(m_dirtyArea, Region(r1.createUnion(r2)));
}
@ -371,8 +372,8 @@ Stroke::Pt ToolLoopManager::getSpriteStrokePt(const Pointer& pointer)
{
// Convert the screen point to a sprite point
Stroke::Pt spritePoint = pointer.point();
spritePoint.size = m_brushSize0;
spritePoint.angle = m_brushAngle0;
spritePoint.size = m_brush0->size();
spritePoint.angle = m_brush0->angle();
// Center the input to some grid point if needed
snapToGrid(spritePoint);

View File

@ -99,10 +99,9 @@ private:
Pointer m_lastPointer;
gfx::Region m_dirtyArea;
gfx::Region m_nextDirtyArea;
const int m_brushSize0;
const int m_brushAngle0;
DynamicsOptions m_dynamics;
gfx::PointF m_stabilizerCenter;
doc::Brush* m_brush0 = nullptr;
};
} // namespace tools

View File

@ -1525,18 +1525,22 @@ protected:
class ContextBar::SymmetryField : public ButtonSet {
public:
SymmetryField() : ButtonSet(3) {
SymmetryField() : ButtonSet(5) {
setMultiMode(MultiMode::Set);
auto* theme = SkinTheme::get(this);
auto theme = SkinTheme::get(this);
addItem(theme->parts.horizontalSymmetry(), theme->styles.symmetryField());
addItem(theme->parts.verticalSymmetry(), theme->styles.symmetryField());
addItem(theme->parts.rightDiagonalSymmetry(), theme->styles.symmetryField());
addItem(theme->parts.leftDiagonalSymmetry(), theme->styles.symmetryField());
addItem("...", theme->styles.symmetryOptions());
}
void setupTooltips(TooltipManager* tooltipManager) {
tooltipManager->addTooltipFor(at(0), Strings::symmetry_toggle_horizontal(), BOTTOM);
tooltipManager->addTooltipFor(at(1), Strings::symmetry_toggle_vertical(), BOTTOM);
tooltipManager->addTooltipFor(at(2), Strings::symmetry_show_options(), BOTTOM);
tooltipManager->addTooltipFor(at(2), Strings::symmetry_toggle_right_diagonal(), BOTTOM);
tooltipManager->addTooltipFor(at(3), Strings::symmetry_toggle_left_diagonal(), BOTTOM);
tooltipManager->addTooltipFor(at(4), Strings::symmetry_show_options(), BOTTOM);
}
void updateWithCurrentDocument() {
@ -1548,6 +1552,8 @@ public:
at(0)->setSelected(int(docPref.symmetry.mode()) & int(app::gen::SymmetryMode::HORIZONTAL) ? true: false);
at(1)->setSelected(int(docPref.symmetry.mode()) & int(app::gen::SymmetryMode::VERTICAL) ? true: false);
at(2)->setSelected(int(docPref.symmetry.mode()) & int(app::gen::SymmetryMode::RIGHT_DIAG) ? true: false);
at(3)->setSelected(int(docPref.symmetry.mode()) & int(app::gen::SymmetryMode::LEFT_DIAG) ? true: false);
}
private:
@ -1561,16 +1567,70 @@ private:
DocumentPreferences& docPref =
Preferences::instance().document(doc);
auto oldMode = docPref.symmetry.mode();
int mode = 0;
if (at(0)->isSelected()) mode |= int(app::gen::SymmetryMode::HORIZONTAL);
if (at(1)->isSelected()) mode |= int(app::gen::SymmetryMode::VERTICAL);
if (at(2)->isSelected()) mode |= int(app::gen::SymmetryMode::RIGHT_DIAG);
if (at(3)->isSelected()) mode |= int(app::gen::SymmetryMode::LEFT_DIAG);
// Non sense symmetries filter:
// - H + 1Diag
// - V + 1Diag
// - H + V + 1Diag
const bool HorV = (mode & int(app::gen::SymmetryMode::HORIZONTAL)) ||
(mode & int(app::gen::SymmetryMode::VERTICAL));
const bool HxorV = !(mode & int(app::gen::SymmetryMode::HORIZONTAL)) !=
!(mode & int(app::gen::SymmetryMode::VERTICAL));
const bool RDxorLD = !(mode & int(app::gen::SymmetryMode::RIGHT_DIAG)) !=
!(mode & int(app::gen::SymmetryMode::LEFT_DIAG));
if (oldMode == gen::SymmetryMode::HORIZONTAL ||
oldMode == gen::SymmetryMode::VERTICAL ||
oldMode == gen::SymmetryMode::BOTH) {
if (HorV && RDxorLD) {
mode = int(app::gen::SymmetryMode::ALL);
at(0)->setSelected(true);
at(1)->setSelected(true);
at(2)->setSelected(true);
at(3)->setSelected(true);
}
}
else if (oldMode == gen::SymmetryMode::ALL) {
if (HxorV) {
mode = int(app::gen::SymmetryMode::BOTH_DIAG);
at(0)->setSelected(false);
at(1)->setSelected(false);
}
else if (RDxorLD) {
mode = int(app::gen::SymmetryMode::BOTH);
at(2)->setSelected(false);
at(3)->setSelected(false);
}
}
else if ((oldMode == gen::SymmetryMode::RIGHT_DIAG ||
oldMode == gen::SymmetryMode::LEFT_DIAG ||
oldMode == gen::SymmetryMode::BOTH_DIAG) &&
HorV) {
mode = int(app::gen::SymmetryMode::ALL);
at(0)->setSelected(true);
at(1)->setSelected(true);
at(2)->setSelected(true);
at(3)->setSelected(true);
}
// Non sense symmetries filter end
if (at(0)->isSelected()) mode |= int(app::gen::SymmetryMode::HORIZONTAL);
if (at(1)->isSelected()) mode |= int(app::gen::SymmetryMode::VERTICAL);
if (at(2)->isSelected()) mode |= int(app::gen::SymmetryMode::RIGHT_DIAG);
if (at(3)->isSelected()) mode |= int(app::gen::SymmetryMode::LEFT_DIAG);
if (app::gen::SymmetryMode(mode) != docPref.symmetry.mode()) {
docPref.symmetry.mode(app::gen::SymmetryMode(mode));
// Redraw symmetry rules
doc->notifyGeneralUpdate();
}
else if (at(2)->isSelected()) {
auto item = at(2);
else if (at(4)->isSelected()) {
auto* item = at(4);
gfx::Rect bounds = item->bounds();
item->setSelected(false);
@ -2429,7 +2489,6 @@ void ContextBar::setActiveBrushBySlot(tools::Tool* tool, int slot)
// the slot.
if (brush.hasFlag(BrushSlot::Flags::ImageColor))
brush.brush()->resetImageColors();
setActiveBrush(brush.brush());
}
else {

View File

@ -955,6 +955,54 @@ void Editor::drawSpriteUnclippedRect(ui::Graphics* g, const gfx::Rect& _rc)
enclosingRect.w);
}
}
if (mode & int(app::gen::SymmetryMode::RIGHT_DIAG)) {
double y = m_docPref.symmetry.yAxis();
double x = m_docPref.symmetry.xAxis();
gfx::Color color = color_utils::color_for_ui(m_docPref.grid.color());
// Bottom point intersection:
gfx::Point bottomLeft(enclosingRect.x + m_proj.applyY(mainTilePosition().x) + int(m_proj.applyX<double>(x))
- (enclosingRect.h - m_proj.applyY(mainTilePosition().y) - int(m_proj.applyY<double>(y))),
enclosingRect.y2());
if (bottomLeft.x < enclosingRect.x) {
// Left intersection
bottomLeft.y = enclosingRect.y2() - enclosingRect.x + bottomLeft.x;
bottomLeft.x = enclosingRect.x;
}
// Top intersection
gfx::Point topRight(enclosingRect.x + m_proj.applyY(mainTilePosition().x) + int(m_proj.applyX<double>(x))
+ m_proj.applyY(mainTilePosition().y) + int(m_proj.applyY<double>(y)),
enclosingRect.y);
if (enclosingRect.x2() < topRight.x) {
// Right intersection
topRight.y = enclosingRect.y + topRight.x - enclosingRect.x2();
topRight.x = enclosingRect.x2();
}
g->drawLine(color, bottomLeft, topRight);
}
if (mode & int(app::gen::SymmetryMode::LEFT_DIAG)) {
double y = m_docPref.symmetry.yAxis();
double x = m_docPref.symmetry.xAxis();
gfx::Color color = color_utils::color_for_ui(m_docPref.grid.color());
// Bottom point intersection:
gfx::Point bottomRight(enclosingRect.x + m_proj.applyY(mainTilePosition().x) + int(m_proj.applyX<double>(x))
+ (enclosingRect.h - m_proj.applyY(mainTilePosition().y) - int(m_proj.applyX<double>(y))),
enclosingRect.y2());
if (enclosingRect.x2() < bottomRight.x) {
// Left intersection
bottomRight.y = enclosingRect.y2() - bottomRight.x + enclosingRect.x2();
bottomRight.x = enclosingRect.x2();
}
// Top intersection
gfx::Point topLeft(enclosingRect.x + m_proj.applyY(mainTilePosition().x) + int(m_proj.applyX<double>(x))
- m_proj.applyY(mainTilePosition().y) - int(m_proj.applyY<double>(y)),
enclosingRect.y);
if (topLeft.x < enclosingRect.x) {
// Right intersection
topLeft.y = enclosingRect.y + enclosingRect.x - topLeft.x;
topLeft.x = enclosingRect.x;
}
g->drawLine(color, topLeft, bottomRight);
}
}
// Draw active layer/cel edges

View File

@ -1107,7 +1107,9 @@ bool StandbyState::Decorator::getSymmetryHandles(Editor* editor, Handles& handle
auto theme = skin::SkinTheme::get(editor);
os::Surface* part = theme->parts.transformationHandle()->bitmap(0);
if (int(mode) & int(app::gen::SymmetryMode::HORIZONTAL)) {
if ((int(mode) & int(app::gen::SymmetryMode::HORIZONTAL)) ||
(int(mode) & int(app::gen::SymmetryMode::RIGHT_DIAG)) ||
(int(mode) & int(app::gen::SymmetryMode::LEFT_DIAG))) {
double pos = symmetry.xAxis();
gfx::PointF pt1, pt2;
@ -1128,7 +1130,9 @@ bool StandbyState::Decorator::getSymmetryHandles(Editor* editor, Handles& handle
gfx::Rect(int(pt2.x), int(pt2.y), part->width(), part->height())));
}
if (int(mode) & int(app::gen::SymmetryMode::VERTICAL)) {
if ((int(mode) & int(app::gen::SymmetryMode::VERTICAL)) ||
(int(mode) & int(app::gen::SymmetryMode::RIGHT_DIAG)) ||
(int(mode) & int(app::gen::SymmetryMode::LEFT_DIAG))) {
double pos = symmetry.yAxis();
gfx::PointF pt1, pt2;