Repository: LibreSprite/LibreSprite (language: cpp)

Issue:
mirror/symentry only works at horizontal or vertical at one time
![2021-01-29 14-53-29](https://user-images.githubusercontent.com/74117079/106261881-010f2500-6244-11eb-84da-fb61b5acab98.gif)

We need to have both horizontal and vertical symmetry at the same time
Honestly, I really need this feature


Discussion / hints:

Fix patch:
diff --git a/data/pref.xml b/data/pref.xml
index 1ed24e692..385532075 100644
--- a/data/pref.xml
+++ b/data/pref.xml
@@ -76,6 +76,7 @@
       <value id="NONE" value="0" />
       <value id="HORIZONTAL" value="1" />
       <value id="VERTICAL" value="2" />
+      <value id="BOTH" value="3" />
     </enum>
   </types>
 
diff --git a/data/skins/default/sheet.png b/data/skins/default/sheet.png
index bc0ab7ac1..44d2d0701 100644
Binary files a/data/skins/default/sheet.png and b/data/skins/default/sheet.png differ
diff --git a/data/skins/default/skin.xml b/data/skins/default/skin.xml
index 3ca67bdf4..c354e4556 100644
--- a/data/skins/default/skin.xml
+++ b/data/skins/default/skin.xml
@@ -394,7 +394,6 @@
     <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="icon_arrow_down"                  x="144" y="256" w="7" h="4" />
diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt
index 32b7f38d5..9318960a6 100644
--- a/src/app/CMakeLists.txt
+++ b/src/app/CMakeLists.txt
@@ -342,7 +342,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
   transaction.cpp
diff --git a/src/app/tools/symmetries.h b/src/app/tools/symmetries.h
deleted file mode 100644
index 0ca64d1ce..000000000
--- a/src/app/tools/symmetries.h
+++ /dev/null
@@ -1,39 +0,0 @@
-// Aseprite
-// Copyright (C) 2015  David Capello
-//
-// This program is free software; you can redistribute it and/or modify
-// it under the terms of the GNU General Public License version 2 as
-// published by the Free Software Foundation.
-
-#ifndef APP_TOOLS_SYMMETRIES_H_INCLUDED
-#define APP_TOOLS_SYMMETRIES_H_INCLUDED
-#pragma once
-
-#include "app/tools/stroke.h"
-#include "app/tools/symmetry.h"
-
-namespace app {
-namespace tools {
-
-class HorizontalSymmetry : public Symmetry {
-public:
-  HorizontalSymmetry(int x) : m_x(x) { }
-  void generateStrokes(const Stroke& mainStroke, Strokes& strokes,
-                       ToolLoop* loop) override;
-private:
-  int m_x;
-};
-
-class VerticalSymmetry : public Symmetry {
-public:
-  VerticalSymmetry(int y) : m_y(y) { }
-  void generateStrokes(const Stroke& mainStroke, Strokes& strokes,
-                       ToolLoop* loop) override;
-private:
-  int m_y;
-};
-
-} // namespace tools
-} // namespace app
-
-#endif
diff --git a/src/app/tools/symmetries.cpp b/src/app/tools/symmetry.cpp
similarity index 75%
rename from src/app/tools/symmetries.cpp
rename to src/app/tools/symmetry.cpp
index 2d2ccee05..04732ce4f 100644
--- a/src/app/tools/symmetries.cpp
+++ b/src/app/tools/symmetry.cpp
@@ -9,7 +9,7 @@
 #include "config.h"
 #endif
 
-#include "app/tools/symmetries.h"
+#include "app/tools/symmetry.h"
 
 #include "app/tools/stroke.h"
 #include "app/tools/tool_loop.h"
@@ -44,5 +44,14 @@ void VerticalSymmetry::generateStrokes(const Stroke& mainStroke, Strokes& stroke
   strokes.push_back(stroke2);
 }
 
+void DoubleSymmetry::generateStrokes(const Stroke& mainStroke, Strokes& strokes,
+                                       ToolLoop* loop)
+{
+  Strokes strokeSymmetry;
+  HorizontalSymmetry(m_x).generateStrokes(mainStroke, strokeSymmetry, loop);
+  for (const Stroke& stroke : strokeSymmetry)
+    VerticalSymmetry(m_y).generateStrokes(stroke, strokes, loop);
+}
+
 } // namespace tools
 } // namespace app
diff --git a/src/app/tools/symmetry.h b/src/app/tools/symmetry.h
index d5138fa43..4395aaaf1 100644
--- a/src/app/tools/symmetry.h
+++ b/src/app/tools/symmetry.h
@@ -27,6 +27,34 @@ namespace app {
       virtual void generateStrokes(const Stroke& stroke, Strokes& strokes, ToolLoop* loop) = 0;
     };
 
+    class HorizontalSymmetry : public Symmetry {
+    public:
+      HorizontalSymmetry(int x) : m_x(x) { }
+      void generateStrokes(const Stroke& mainStroke, Strokes& strokes,
+                           ToolLoop* loop) override;
+    private:
+      int m_x;
+    };
+
+    class VerticalSymmetry : public Symmetry {
+    public:
+      VerticalSymmetry(int y) : m_y(y) { }
+      void generateStrokes(const Stroke& mainStroke, Strokes& strokes,
+                           ToolLoop* loop) override;
+    private:
+      int m_y;
+    };
+
+    class DoubleSymmetry : public Symmetry {
+    public:
+      DoubleSymmetry(int x, int y) : m_x(x), m_y(y) { }
+      void generateStrokes(const Stroke& mainStroke, Strokes& strokes,
+                           ToolLoop* loop) override;
+    private:
+      int m_x;
+      int m_y;
+    };
+
   } // namespace tools
 } // namespace app
 
diff --git a/src/app/ui/context_bar.cpp b/src/app/ui/context_bar.cpp
index 465094a51..51a10ee1c 100644
--- a/src/app/ui/context_bar.cpp
+++ b/src/app/ui/context_bar.cpp
@@ -1264,17 +1264,17 @@ class ContextBar::AutoSelectLayerField : public CheckBox
 
 class ContextBar::SymmetryField : public ButtonSet {
 public:
-  SymmetryField() : ButtonSet(3) {
+  SymmetryField() : ButtonSet(2) {
+    setMultipleSelection(true);
+
     SkinTheme* theme = SkinTheme::instance();
-    addItem(theme->parts.noSymmetry());
     addItem(theme->parts.horizontalSymmetry());
     addItem(theme->parts.verticalSymmetry());
   }
 
   void setupTooltips(TooltipManager* tooltipManager) {
-    tooltipManager->addTooltipFor(at(0), "Without Symmetry", BOTTOM);
-    tooltipManager->addTooltipFor(at(1), "Horizontal Symmetry", BOTTOM);
-    tooltipManager->addTooltipFor(at(2), "Vertical Symmetry", BOTTOM);
+    tooltipManager->addTooltipFor(at(0), "Horizontal Symmetry", BOTTOM);
+    tooltipManager->addTooltipFor(at(1), "Vertical Symmetry", BOTTOM);
   }
 
   void updateWithCurrentDocument() {
@@ -1283,8 +1283,12 @@ class ContextBar::SymmetryField : public ButtonSet {
       return;
 
     DocumentPreferences& docPref = Preferences::instance().document(doc);
+    app::gen::SymmetryMode symmetryMode = docPref.symmetry.mode();
 
-    setSelectedItem((int)docPref.symmetry.mode());
+    at(0)->setSelected(symmetryMode == app::gen::SymmetryMode::HORIZONTAL
+                        || symmetryMode == app::gen::SymmetryMode::BOTH);
+    at(1)->setSelected(symmetryMode == app::gen::SymmetryMode::VERTICAL
+                        || symmetryMode == app::gen::SymmetryMode::BOTH);
   }
 
 private:
@@ -1298,7 +1302,14 @@ class ContextBar::SymmetryField : public ButtonSet {
     DocumentPreferences& docPref =
       Preferences::instance().document(doc);
 
-    docPref.symmetry.mode((app::gen::SymmetryMode)selectedItem());
+    if (selectedItem() == -1)
+      docPref.symmetry.mode(app::gen::SymmetryMode::NONE);
+    else if (at(0)->isSelected() && at(1)->isSelected())
+      docPref.symmetry.mode(app::gen::SymmetryMode::BOTH);
+    else if (at(0)->isSelected())
+      docPref.symmetry.mode(app::gen::SymmetryMode::HORIZONTAL);
+    else
+      docPref.symmetry.mode(app::gen::SymmetryMode::VERTICAL);
 
     // Redraw symmetry rules
     doc->notifyGeneralUpdate();
diff --git a/src/app/ui/editor/editor.cpp b/src/app/ui/editor/editor.cpp
index 653ac6a55..9551be8b4 100644
--- a/src/app/ui/editor/editor.cpp
+++ b/src/app/ui/editor/editor.cpp
@@ -648,31 +648,26 @@ void Editor::drawSpriteUnclippedRect(ui::Graphics* g, const gfx::Rect& _rc)
   if (isActive() &&
       (m_flags & Editor::kShowSymmetryLine) &&
       Preferences::instance().symmetryMode.enabled()) {
-    switch (m_docPref.symmetry.mode()) {
-      case app::gen::SymmetryMode::NONE:
-        // Do nothing
-        break;
-      case app::gen::SymmetryMode::HORIZONTAL: {
-        int x = m_docPref.symmetry.xAxis();
-        if (x > 0) {
-          gfx::Color color = color_utils::color_for_ui(m_docPref.grid.color());
-          g->drawVLine(color,
-                       spriteRect.x + m_zoom.apply(x),
-                       enclosingRect.y,
-                       enclosingRect.h);
-        }
-        break;
+
+    auto symmetryMode = (int)m_docPref.symmetry.mode();
+    if (symmetryMode & (int)app::gen::SymmetryMode::HORIZONTAL) {
+      int x = m_docPref.symmetry.xAxis();
+      if (x > 0) {
+        gfx::Color color = color_utils::color_for_ui(m_docPref.grid.color());
+        g->drawVLine(color,
+                     spriteRect.x + m_zoom.apply(x),
+                     enclosingRect.y,
+                     enclosingRect.h);
       }
-      case app::gen::SymmetryMode::VERTICAL: {
-        int y = m_docPref.symmetry.yAxis();
-        if (y > 0) {
-          gfx::Color color = color_utils::color_for_ui(m_docPref.grid.color());
-          g->drawHLine(color,
-                       enclosingRect.x,
-                       spriteRect.y + m_zoom.apply(y),
-                       enclosingRect.w);
-        }
-        break;
+    }
+    if (symmetryMode & (int)app::gen::SymmetryMode::VERTICAL) {
+      int y = m_docPref.symmetry.yAxis();
+      if (y > 0) {
+        gfx::Color color = color_utils::color_for_ui(m_docPref.grid.color());
+        g->drawHLine(color,
+                     enclosingRect.x,
+                     spriteRect.y + m_zoom.apply(y),
+                     enclosingRect.w);
       }
     }
   }
diff --git a/src/app/ui/editor/moving_symmetry_state.cpp b/src/app/ui/editor/moving_symmetry_state.cpp
index 490b0ed84..b5eb2b298 100644
--- a/src/app/ui/editor/moving_symmetry_state.cpp
+++ b/src/app/ui/editor/moving_symmetry_state.cpp
@@ -20,11 +20,11 @@ namespace app {
 using namespace ui;
 
 MovingSymmetryState::MovingSymmetryState(Editor* editor, MouseMessage* msg,
-                                         app::gen::SymmetryMode mode,
-                                         Option<int>& symmetryAxis)
-  : m_symmetryMode(mode)
-  , m_symmetryAxis(symmetryAxis)
-  , m_symmetryAxisStart(symmetryAxis())
+                                         Axis axis,
+                                         Option<int>& axisPos)
+  : m_symmetryAxis(axis)
+  , m_symmetryAxisPos(axisPos)
+  , m_symmetryAxisStart(axisPos())
 {
   m_mouseStart = editor->screenToEditor(msg->position());
   editor->captureMouse();
@@ -47,17 +47,15 @@ bool MovingSymmetryState::onMouseMove(Editor* editor, MouseMessage* msg)
   gfx::Point delta = newCursorPos - m_mouseStart;
   int pos = 0;
 
-  switch (m_symmetryMode) {
-    case app::gen::SymmetryMode::HORIZONTAL:
-      pos = m_symmetryAxisStart + delta.x;
-      pos = MID(1, pos, editor->sprite()->width()-1);
-      break;
-    case app::gen::SymmetryMode::VERTICAL:
-      pos = m_symmetryAxisStart + delta.y;
-      pos = MID(1, pos, editor->sprite()->height()-1);
-      break;
+  if (m_symmetryAxis == Axis::HORIZONTAL) {
+    pos = m_symmetryAxisStart + delta.x;
+    pos = MID(1, pos, editor->sprite()->width()-1);
   }
-  m_symmetryAxis(pos);
+  else {
+    pos = m_symmetryAxisStart + delta.y;
+    pos = MID(1, pos, editor->sprite()->height()-1);
+  }
+  m_symmetryAxisPos(pos);
 
   // Redraw the editor.
   editor->invalidate();
@@ -68,14 +66,14 @@ bool MovingSymmetryState::onMouseMove(Editor* editor, MouseMessage* msg)
 
 bool MovingSymmetryState::onUpdateStatusBar(Editor* editor)
 {
-  if (m_symmetryMode == app::gen::SymmetryMode::HORIZONTAL)
+  if (m_symmetryAxis == Axis::HORIZONTAL)
     StatusBar::instance()->setStatusText
-      (0, "Left %3d Right %3d", m_symmetryAxis(),
-       editor->sprite()->width() - m_symmetryAxis());
+      (0, "Left %3d Right %3d", m_symmetryAxisPos(),
+       editor->sprite()->width() - m_symmetryAxisPos());
   else
     StatusBar::instance()->setStatusText
-      (0, "Top %3d Bottom %3d", m_symmetryAxis(),
-       editor->sprite()->height() - m_symmetryAxis());
+      (0, "Top %3d Bottom %3d", m_symmetryAxisPos(),
+       editor->sprite()->height() - m_symmetryAxisPos());
 
   return true;
 }
diff --git a/src/app/ui/editor/moving_symmetry_state.h b/src/app/ui/editor/moving_symmetry_state.h
index df2c29c93..05840581a 100644
--- a/src/app/ui/editor/moving_symmetry_state.h
+++ b/src/app/ui/editor/moving_symmetry_state.h
@@ -11,6 +11,7 @@
 
 #include "app/pref/preferences.h"
 #include "app/ui/editor/standby_state.h"
+#include "app/ui/editor/symmetry_handles.h"
 
 namespace app {
   class Editor;
@@ -18,8 +19,8 @@ namespace app {
   class MovingSymmetryState : public StandbyState {
   public:
     MovingSymmetryState(Editor* editor, ui::MouseMessage* msg,
-                        app::gen::SymmetryMode mode,
-                        Option<int>& symmetryAxis);
+                        Axis axis,
+                        Option<int>& axisPos);
     virtual ~MovingSymmetryState();
 
     virtual bool onMouseUp(Editor* editor, ui::MouseMessage* msg) override;
@@ -29,8 +30,8 @@ namespace app {
     virtual bool requireBrushPreview() override { return false; }
 
   private:
-    app::gen::SymmetryMode m_symmetryMode;
-    Option<int>& m_symmetryAxis;
+    Axis m_symmetryAxis;
+    Option<int>& m_symmetryAxisPos;
     int m_symmetryAxisStart;
     gfx::Point m_mouseStart;
   };
diff --git a/src/app/ui/editor/standby_state.cpp b/src/app/ui/editor/standby_state.cpp
index 33dc864f6..56c6e921c 100644
--- a/src/app/ui/editor/standby_state.cpp
+++ b/src/app/ui/editor/standby_state.cpp
@@ -26,7 +26,6 @@
 #include "app/ui/editor/drawing_state.h"
 #include "app/ui/editor/editor.h"
 #include "app/ui/editor/editor_customization_delegate.h"
-#include "app/ui/editor/handle_type.h"
 #include "app/ui/editor/moving_cel_state.h"
 #include "app/ui/editor/moving_pixels_state.h"
 #include "app/ui/editor/moving_symmetry_state.h"
@@ -281,19 +280,19 @@ bool StandbyState::onMouseDown(Editor* editor, MouseMessage* msg)
   }
 
   // Move symmetry
-  gfx::Rect box1, box2;
-  if (m_decorator->getSymmetryHandles(editor, box1, box2) &&
-      (box1.contains(msg->position()) ||
-       box2.contains(msg->position()))) {
-    auto& symmetry = Preferences::instance().document(editor->document()).symmetry;
-    auto mode = symmetry.mode();
-    bool horz = (mode == app::gen::SymmetryMode::HORIZONTAL);
-    auto& axis = (horz ? symmetry.xAxis:
-                         symmetry.yAxis);
-    editor->setState(
-      EditorStatePtr(new MovingSymmetryState(editor, msg,
-                                             mode, axis)));
-    return true;
+  SymmetryHandles handles;
+  if (m_decorator->getSymmetryHandles(editor, handles)) {
+    for (auto& handle : handles) {
+      if (handle.rect().contains(msg->position())) {
+        auto& symmetry = Preferences::instance().document(editor->document()).symmetry;
+        const auto& axis = handle.axis();
+        auto& axisPos = axis == Axis::HORIZONTAL ? symmetry.xAxis : symmetry.yAxis;
+
+        editor->setState(
+          EditorStatePtr(new MovingSymmetryState(editor, msg, axis, axisPos)));
+        return true;
+      }
+    }
   }
 
   // Start the Tool-Loop
@@ -674,21 +673,18 @@ bool StandbyState::Decorator::onSetCursor(tools::Ink* ink, Editor* editor, const
     return true;
   }
 
-  gfx::Rect box1, box2;
-  if (getSymmetryHandles(editor, box1, box2) &&
-      (box1.contains(mouseScreenPos) ||
-       box2.contains(mouseScreenPos))) {
-    switch (Preferences::instance().document(editor->document()).symmetry.mode()) {
-      case app::gen::SymmetryMode::HORIZONTAL:
-        editor->showMouseCursor(kSizeWECursor);
-        break;
-      case app::gen::SymmetryMode::VERTICAL:
-        editor->showMouseCursor(kSizeNSCursor);
-        break;
+  SymmetryHandles handles;
+  if (getSymmetryHandles(editor, handles)) {
+    for (auto& handle : handles) {
+      if (handle.rect().contains(mouseScreenPos)) {
+        if (handle.axis() == Axis::HORIZONTAL)
+          editor->showMouseCursor(kSizeWECursor);
+        else
+          editor->showMouseCursor(kSizeNSCursor);
+        return true;
+      }
     }
-    return true;
   }
-
   return false;
 }
 
@@ -718,68 +714,78 @@ void StandbyState::Decorator::postRenderDecorator(EditorPostRender* render)
   }
 
   // Draw transformation handles (if the mask is visible and isn't frozen).
-  gfx::Rect box1, box2;
-  if (StandbyState::Decorator::getSymmetryHandles(editor, box1, box2)) {
+  SymmetryHandles handles;
+  if (StandbyState::Decorator::getSymmetryHandles(editor, handles)) {
     skin::SkinTheme* theme = static_cast<skin::SkinTheme*>(CurrentTheme::get());
     she::Surface* part = theme->parts.transformationHandle()->bitmap(0);
     ScreenGraphics g;
-    g.drawRgbaSurface(part, box1.x, box1.y);
-    g.drawRgbaSurface(part, box2.x, box2.y);
+    for (auto& handle : handles)
+      g.drawRgbaSurface(part, handle.rect().x, handle.rect().y);
   }
 }
 
 void StandbyState::Decorator::getInvalidDecoratoredRegion(Editor* editor, gfx::Region& region)
 {
-  gfx::Rect box1, box2;
-  if (getSymmetryHandles(editor, box1, box2)) {
-    region.createUnion(region, gfx::Region(box1));
-    region.createUnion(region, gfx::Region(box2));
+  SymmetryHandles handles;
+  if (getSymmetryHandles(editor, handles)) {
+    for (auto& handle : handles)
+      region.createUnion(region, gfx::Region(handle.rect()));
   }
 }
 
-bool StandbyState::Decorator::getSymmetryHandles(Editor* editor, gfx::Rect& box1, gfx::Rect& box2)
+bool StandbyState::Decorator::getSymmetryHandles(Editor* editor, SymmetryHandles& handles)
 {
   // Draw transformation handles (if the mask is visible and isn't frozen).
   if (editor->isActive() &&
       editor->editorFlags() & Editor::kShowSymmetryLine &&
       Preferences::instance().symmetryMode.enabled()) {
     const auto& symmetry = Preferences::instance().document(editor->document()).symmetry;
-    auto mode = symmetry.mode();
-    if (mode != app::gen::SymmetryMode::NONE) {
-      bool horz = (mode == app::gen::SymmetryMode::HORIZONTAL);
-      int pos = (horz ? symmetry.xAxis():
-                        symmetry.yAxis());
-      gfx::Rect spriteBounds = editor->sprite()->bounds();
-      gfx::Rect editorViewport = View::getView(editor)->viewportBounds();
-      skin::SkinTheme* theme = static_cast<skin::SkinTheme*>(CurrentTheme::get());
-      she::Surface* part = theme->parts.transformationHandle()->bitmap(0);
-      gfx::Point pt1, pt2;
-
-      if (horz) {
-        pt1 = gfx::Point(spriteBounds.x+pos, spriteBounds.y);
-        pt1 = editor->editorToScreen(pt1);
-        pt2 = gfx::Point(spriteBounds.x+pos, spriteBounds.y+spriteBounds.h);
-        pt2 = editor->editorToScreen(pt2);
-        pt1.y = std::max(pt1.y-part->height(), editorViewport.y);
-        pt2.y = std::min(pt2.y, editorViewport.point2().y-part->height());
-        pt1.x -= part->width()/2;
-        pt2.x -= part->width()/2;
-      }
-      else {
-        pt1 = gfx::Point(spriteBounds.x, spriteBounds.y+pos);
-        pt1 = editor->editorToScreen(pt1);
-        pt2 = gfx::Point(spriteBounds.x+spriteBounds.w, spriteBounds.y+pos);
-        pt2 = editor->editorToScreen(pt2);
-        pt1.x = std::max(pt1.x-part->width(), editorViewport.x);
-        pt2.x = std::min(pt2.x, editorViewport.point2().x-part->width());
-        pt1.y -= part->height()/2;
-        pt2.y -= part->height()/2;
-      }
+    auto mode = (int)symmetry.mode();
 
-      box1 = gfx::Rect(pt1.x, pt1.y, part->width(), part->height());
-      box2 = gfx::Rect(pt2.x, pt2.y, part->width(), part->height());
-      return true;
+    if (mode == (int)app::gen::SymmetryMode::NONE)
+      return false;
+
+    gfx::Rect spriteBounds = editor->sprite()->bounds();
+    gfx::Rect editorViewport = View::getView(editor)->viewportBounds();
+    skin::SkinTheme* theme = static_cast<skin::SkinTheme*>(CurrentTheme::get());
+    she::Surface* part = theme->parts.transformationHandle()->bitmap(0);
+    gfx::Point pt1, pt2;
+
+    if (mode & (int)app::gen::SymmetryMode::HORIZONTAL) {
+      int pos = symmetry.xAxis();
+      pt1 = gfx::Point(spriteBounds.x + pos, spriteBounds.y);
+      pt1 = editor->editorToScreen(pt1);
+      pt2 = gfx::Point(spriteBounds.x+ pos, spriteBounds.y+spriteBounds.h);
+      pt2 = editor->editorToScreen(pt2);
+      pt1.y = std::max(pt1.y-part->height(), editorViewport.y);
+      pt2.y = std::min(pt2.y, editorViewport.point2().y-part->height());
+      pt1.x -= part->width()/2;
+      pt2.x -= part->width()/2;
+
+      handles.push_back(
+        SymmetryHandle(gfx::Rect(pt1.x, pt1.y, part->width(), part->height()), Axis::HORIZONTAL));
+      handles.push_back(
+        SymmetryHandle(gfx::Rect(pt2.x, pt2.y, part->width(), part->height()), Axis::HORIZONTAL));
+    }
+
+    if (mode & (int)app::gen::SymmetryMode::VERTICAL) {
+      int pos = symmetry.yAxis();
+      pt1 = gfx::Point(spriteBounds.x, spriteBounds.y + pos);
+      pt1 = editor->editorToScreen(pt1);
+      pt2 = gfx::Point(spriteBounds.x+spriteBounds.w, spriteBounds.y + pos);
+      pt2 = editor->editorToScreen(pt2);
+      pt1.x = std::max(pt1.x-part->width(), editorViewport.x);
+      pt2.x = std::min(pt2.x, editorViewport.point2().x-part->width());
+      pt1.y -= part->height()/2;
+      pt2.y -= part->height()/2;
+
+      handles.push_back(
+        SymmetryHandle(gfx::Rect(pt1.x, pt1.y, part->width(), part->height()), Axis::VERTICAL));
+      handles.push_back(
+        SymmetryHandle(gfx::Rect(pt2.x, pt2.y, part->width(), part->height()), Axis::VERTICAL));
     }
+
+    return true;
   }
   return false;
 }
diff --git a/src/app/ui/editor/standby_state.h b/src/app/ui/editor/standby_state.h
index 1cf0d7336..6e6a4c0f1 100644
--- a/src/app/ui/editor/standby_state.h
+++ b/src/app/ui/editor/standby_state.h
@@ -13,6 +13,7 @@
 #include "app/ui/editor/editor_decorator.h"
 #include "app/ui/editor/handle_type.h"
 #include "app/ui/editor/state_with_wheel_behavior.h"
+#include "app/ui/editor/symmetry_handles.h"
 #include "base/connection.h"
 
 namespace app {
@@ -58,7 +59,7 @@ namespace app {
       virtual ~Decorator();
 
       TransformHandles* getTransformHandles(Editor* editor);
-      bool getSymmetryHandles(Editor* editor, gfx::Rect& box1, gfx::Rect& box2);
+      bool getSymmetryHandles(Editor* editor, SymmetryHandles& handles);
 
       bool onSetCursor(tools::Ink* ink, Editor* editor, const gfx::Point& mouseScreenPos);
 
diff --git a/src/app/ui/editor/symmetry_handles.h b/src/app/ui/editor/symmetry_handles.h
new file mode 100644
index 000000000..174ff9e10
--- /dev/null
+++ b/src/app/ui/editor/symmetry_handles.h
@@ -0,0 +1,40 @@
+// LibreSprite
+// Copyright (C) 2021 LibreSprite
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+
+#ifndef APP_UI_EDITOR_SYMMETRY_HANDLES_H_INCLUDED
+#define APP_UI_EDITOR_SYMMETRY_HANDLES_H_INCLUDED
+#pragma once
+
+#include "gfx/rect.h"
+
+#include <vector>
+
+namespace app {
+
+    enum Axis {
+      HORIZONTAL,
+      VERTICAL
+    };
+
+    class SymmetryHandle {
+    public:
+      SymmetryHandle(const gfx::Rect& rect, const Axis axis) : m_rect(rect), m_axis(axis) {
+      }
+
+      gfx::Rect rect() { return m_rect; }
+      Axis axis() { return m_axis; }
+
+    private:
+      gfx::Rect m_rect;
+      Axis m_axis;
+    };
+
+    typedef std::vector<SymmetryHandle> SymmetryHandles;
+
+} // namespace app
+
+#endif
diff --git a/src/app/ui/editor/tool_loop_impl.cpp b/src/app/ui/editor/tool_loop_impl.cpp
index 6abc2518c..8df0e4df8 100644
--- a/src/app/ui/editor/tool_loop_impl.cpp
+++ b/src/app/ui/editor/tool_loop_impl.cpp
@@ -26,7 +26,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"
@@ -164,6 +164,10 @@ class ToolLoopBase : public tools::ToolLoop {
         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::DoubleSymmetry(m_docPref.symmetry.xAxis(), m_docPref.symmetry.yAxis()));
+          break;
       }
     }
 


Test patch: