[aseprite] 203/250: Add symmetry mode (fix #208)

Tobias Hansen thansen at moszumanska.debian.org
Sun Dec 20 15:27:31 UTC 2015


This is an automated email from the git hooks/post-receive script.

thansen pushed a commit to branch master
in repository aseprite.

commit 56854cdb9f1bd8eb397a918ef2f554636f55724b
Author: David Capello <davidcapello at gmail.com>
Date:   Mon Oct 26 17:51:32 2015 -0300

    Add symmetry mode (fix #208)
    
    This is a first iteration of the feature, it doesn’t have handles to
    move the symmetry line and it only contains two symmetry modes:
    horizontal or vertical.
    
    As an extra change, we have added the new Stroke type to wrap a vector
    of gfx::Points and simplify some existing code in the ToolLoop.
---
 data/gui.xml                           |   1 +
 data/pref.xml                          |  13 ++
 data/skins/default/sheet.png           | Bin 13834 -> 13952 bytes
 data/skins/default/skin.xml            |   3 +
 src/app/CMakeLists.txt                 |   3 +
 src/app/commands/cmd_symmetry_mode.cpp |  60 ++++++++
 src/app/commands/commands_list.h       |   1 +
 src/app/tools/controller.h             |  13 +-
 src/app/tools/controllers.h            | 241 +++++++++++++++++----------------
 src/app/tools/intertwine.h             |   5 +-
 src/app/tools/intertwiners.h           | 200 ++++++++++++++-------------
 src/app/tools/stroke.cpp               |  62 +++++++++
 src/app/tools/stroke.h                 |  66 +++++++++
 src/app/tools/symmetries.cpp           |  40 ++++++
 src/app/tools/symmetries.h             |  37 +++++
 src/app/tools/symmetry.h               |  31 +++++
 src/app/tools/tool_box.cpp             |   1 +
 src/app/tools/tool_loop.h              |   2 +
 src/app/tools/tool_loop_manager.cpp    |  87 ++++++------
 src/app/tools/tool_loop_manager.h      |  12 +-
 src/app/ui/context_bar.cpp             |  58 ++++++++
 src/app/ui/context_bar.h               |   3 +
 src/app/ui/editor/editor.cpp           |  31 +++++
 src/app/ui/editor/tool_loop_impl.cpp   |  26 ++++
 src/app/ui_context.cpp                 |  13 +-
 25 files changed, 726 insertions(+), 283 deletions(-)

diff --git a/data/gui.xml b/data/gui.xml
index a15d94a..090aa93 100644
--- a/data/gui.xml
+++ b/data/gui.xml
@@ -684,6 +684,7 @@
             <param name="axis" value="y" />
 	  </item>
 	</menu>
+	<item command="SymmetryMode" text="S&ymmetry Options" />
         <separator />
         <item command="SetLoopSection" text="Set &Loop Section" />
         <item command="ShowOnionSkin" text="Show &Onion Skin" />
diff --git a/data/pref.xml b/data/pref.xml
index 83c9230..936bf08 100644
--- a/data/pref.xml
+++ b/data/pref.xml
@@ -64,6 +64,11 @@
       <value id="SOUTH" value="7" />
       <value id="SOUTHEAST" value="8" />
     </enum>
+    <enum id="SymmetryMode">
+      <value id="NONE" value="0" />
+      <value id="HORIZONTAL" value="1" />
+      <value id="VERTICAL" value="2" />
+    </enum>
   </types>
 
   <global>
@@ -159,6 +164,9 @@
       <option id="font_face" type="std::string" />
       <option id="font_size" type="int" default="12" />
     </section>
+    <section id="symmetry_mode">
+      <option id="enabled" type="bool" default="false" />
+    </section>
   </global>
 
   <tool>
@@ -191,6 +199,11 @@
     <section id="tiled">
       <option id="mode" type="filters::TiledMode" default="filters::TiledMode::NONE" migrate="Tools.Tiled" />
     </section>
+    <section id="symmetry">
+      <option id="mode" type="SymmetryMode" default="SymmetryMode::NONE" />
+      <option id="x_axis" type="int" default="0" />
+      <option id="y_axis" type="int" default="0" />
+    </section>
     <section id="grid">
       <option id="snap" type="bool" default="false" migrate="Grid.SnapTo" />
       <option id="visible" type="bool" default="false" migrate="Grid.Visible" />
diff --git a/data/skins/default/sheet.png b/data/skins/default/sheet.png
index af48929..af0bf50 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 ca885b6..2cecd7b 100644
--- a/data/skins/default/skin.xml
+++ b/data/skins/default/skin.xml
@@ -403,6 +403,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" />
   </parts>
 
   <stylesheet>
diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt
index e1d1723..d4e30a0 100644
--- a/src/app/CMakeLists.txt
+++ b/src/app/CMakeLists.txt
@@ -248,6 +248,7 @@ add_library(app-lib
   commands/cmd_sprite_properties.cpp
   commands/cmd_sprite_size.cpp
   commands/cmd_switch_colors.cpp
+  commands/cmd_symmetry_mode.cpp
   commands/cmd_tiled_mode.cpp
   commands/cmd_timeline.cpp
   commands/cmd_toggle_preview.cpp
@@ -315,6 +316,8 @@ add_library(app-lib
   tools/intertwine.cpp
   tools/pick_ink.cpp
   tools/point_shape.cpp
+  tools/stroke.cpp
+  tools/symmetries.cpp
   tools/tool_box.cpp
   tools/tool_loop_manager.cpp
   transaction.cpp
diff --git a/src/app/commands/cmd_symmetry_mode.cpp b/src/app/commands/cmd_symmetry_mode.cpp
new file mode 100644
index 0000000..74957e2
--- /dev/null
+++ b/src/app/commands/cmd_symmetry_mode.cpp
@@ -0,0 +1,60 @@
+// Aseprite
+// Copyright (C) 2001-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.
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "app/app.h"
+#include "app/commands/command.h"
+#include "app/commands/params.h"
+#include "app/context.h"
+#include "app/pref/preferences.h"
+
+namespace app {
+
+class SymmetryModeCommand : public Command {
+public:
+  SymmetryModeCommand();
+  Command* clone() const override { return new SymmetryModeCommand(*this); }
+
+protected:
+  bool onEnabled(Context* context) override;
+  bool onChecked(Context* context) override;
+  void onExecute(Context* context) override;
+};
+
+SymmetryModeCommand::SymmetryModeCommand()
+  : Command("SymmetryMode",
+            "Symmetry Mode",
+            CmdUIOnlyFlag)
+{
+}
+
+bool SymmetryModeCommand::onEnabled(Context* ctx)
+{
+  return ctx->checkFlags(ContextFlags::ActiveDocumentIsWritable |
+                         ContextFlags::HasActiveSprite);
+}
+
+bool SymmetryModeCommand::onChecked(Context* ctx)
+{
+  return Preferences::instance().symmetryMode.enabled();
+}
+
+void SymmetryModeCommand::onExecute(Context* ctx)
+{
+  auto& enabled = Preferences::instance().symmetryMode.enabled;
+  enabled(!enabled());
+}
+
+Command* CommandFactory::createSymmetryModeCommand()
+{
+  return new SymmetryModeCommand;
+}
+
+} // namespace app
diff --git a/src/app/commands/commands_list.h b/src/app/commands/commands_list.h
index c7120c2..00c3edb 100644
--- a/src/app/commands/commands_list.h
+++ b/src/app/commands/commands_list.h
@@ -114,6 +114,7 @@ FOR_EACH_COMMAND(SnapToGrid)
 FOR_EACH_COMMAND(SpriteProperties)
 FOR_EACH_COMMAND(SpriteSize)
 FOR_EACH_COMMAND(SwitchColors)
+FOR_EACH_COMMAND(SymmetryMode)
 FOR_EACH_COMMAND(TiledMode)
 FOR_EACH_COMMAND(Timeline)
 FOR_EACH_COMMAND(TogglePreview)
diff --git a/src/app/tools/controller.h b/src/app/tools/controller.h
index 0408e4a..e0305e2 100644
--- a/src/app/tools/controller.h
+++ b/src/app/tools/controller.h
@@ -18,13 +18,12 @@
 namespace app {
   namespace tools {
 
+    class Stroke;
     class ToolLoop;
 
     // This class controls user input.
     class Controller {
     public:
-      typedef std::vector<gfx::Point> Points;
-
       virtual ~Controller() { }
 
       virtual bool canSnapToGrid() { return true; }
@@ -41,16 +40,16 @@ namespace app {
       // Called when the user starts drawing and each time a new button is
       // pressed. The controller could be sure that this method is called
       // at least one time.
-      virtual void pressButton(Points& points, const gfx::Point& point) = 0;
+      virtual void pressButton(Stroke& stroke, const gfx::Point& point) = 0;
 
       // Called each time a mouse button is released.
-      virtual bool releaseButton(Points& points, const gfx::Point& point) = 0;
+      virtual bool releaseButton(Stroke& stroke, const gfx::Point& point) = 0;
 
       // Called when the mouse is moved.
-      virtual void movement(ToolLoop* loop, Points& points, const gfx::Point& point) = 0;
+      virtual void movement(ToolLoop* loop, Stroke& stroke, const gfx::Point& point) = 0;
 
-      virtual void getPointsToInterwine(const Points& input, Points& output) = 0;
-      virtual void getStatusBarText(const Points& points, std::string& text) = 0;
+      virtual void getStrokeToInterwine(const Stroke& input, Stroke& output) = 0;
+      virtual void getStatusBarText(const Stroke& stroke, std::string& text) = 0;
     };
 
   } // namespace tools
diff --git a/src/app/tools/controllers.h b/src/app/tools/controllers.h
index aa2487a..e6e176f 100644
--- a/src/app/tools/controllers.h
+++ b/src/app/tools/controllers.h
@@ -22,7 +22,7 @@ public:
     m_movingOrigin = false;
   }
 
-  void pressButton(Points& points, const Point& point) override {
+  void pressButton(Stroke& stroke, const Point& point) override {
     m_last = point;
   }
 
@@ -37,13 +37,12 @@ public:
   }
 
 protected:
-  bool isMovingOrigin(Points& points, const Point& point) {
+  bool isMovingOrigin(Stroke& stroke, const Point& point) {
     bool used = false;
 
     if (m_movingOrigin) {
       Point delta = (point - m_last);
-      for (auto& p : points)
-        p += delta;
+      stroke.offset(delta);
 
       onMoveOrigin(delta);
       used = true;
@@ -80,38 +79,39 @@ class FreehandController : public Controller {
 public:
   bool isFreehand() override { return true; }
 
-  void pressButton(Points& points, const Point& point) override {
-    points.push_back(point);
+  void pressButton(Stroke& stroke, const Point& point) override {
+    stroke.addPoint(point);
   }
 
-  bool releaseButton(Points& points, const Point& point) override {
+  bool releaseButton(Stroke& stroke, const Point& point) override {
     return false;
   }
 
-  void movement(ToolLoop* loop, Points& points, const Point& point) override {
-    points.push_back(point);
+  void movement(ToolLoop* loop, Stroke& stroke, const Point& point) override {
+    stroke.addPoint(point);
   }
 
-  void getPointsToInterwine(const Points& input, Points& output) override {
+  void getStrokeToInterwine(const Stroke& input, Stroke& output) override {
     if (input.size() == 1) {
-      output.push_back(input[0]);
+      output.addPoint(input[0]);
     }
     else if (input.size() >= 2) {
-      output.push_back(input[input.size()-2]);
-      output.push_back(input[input.size()-1]);
+      output.addPoint(input[input.size()-2]);
+      output.addPoint(input[input.size()-1]);
     }
   }
 
-  void getStatusBarText(const Points& points, std::string& text) override {
-    ASSERT(!points.empty());
-    if (points.empty())
+  void getStatusBarText(const Stroke& stroke, std::string& text) override {
+    ASSERT(!stroke.empty());
+    if (stroke.empty())
       return;
 
     char buf[1024];
     sprintf(buf, "Start %3d %3d End %3d %3d",
-            points[0].x, points[0].y,
-            points[points.size()-1].x,
-            points[points.size()-1].y);
+            stroke.firstPoint().x,
+            stroke.firstPoint().y,
+            stroke.lastPoint().x,
+            stroke.lastPoint().y);
     text = buf;
   }
 
@@ -127,16 +127,16 @@ public:
     m_fromCenter = (modifiers & ui::kKeyCtrlModifier) ? true: false;
   }
 
-  void pressButton(Points& points, const Point& point) override {
-    MoveOriginCapability::pressButton(points, point);
+  void pressButton(Stroke& stroke, const Point& point) override {
+    MoveOriginCapability::pressButton(stroke, point);
 
     m_first = point;
 
-    points.push_back(point);
-    points.push_back(point);
+    stroke.addPoint(point);
+    stroke.addPoint(point);
   }
 
-  bool releaseButton(Points& points, const Point& point) override {
+  bool releaseButton(Stroke& stroke, const Point& point) override {
     return false;
   }
 
@@ -154,19 +154,19 @@ public:
     return processKey(key, false);
   }
 
-  void movement(ToolLoop* loop, Points& points, const Point& point) override {
-    ASSERT(points.size() >= 2);
-    if (points.size() < 2)
+  void movement(ToolLoop* loop, Stroke& stroke, const Point& point) override {
+    ASSERT(stroke.size() >= 2);
+    if (stroke.size() < 2)
       return;
 
-    if (MoveOriginCapability::isMovingOrigin(points, point))
+    if (MoveOriginCapability::isMovingOrigin(stroke, point))
       return;
 
-    points[1] = point;
+    stroke[1] = point;
 
     if (m_squareAspect) {
-      int dx = points[1].x - m_first.x;
-      int dy = points[1].y - m_first.y;
+      int dx = stroke[1].x - m_first.x;
+      int dy = stroke[1].y - m_first.y;
       int minsize = MIN(ABS(dx), ABS(dy));
       int maxsize = MAX(ABS(dx), ABS(dy));
 
@@ -178,84 +178,84 @@ public:
 
         // Snap horizontally
         if (angle < 18.0) {
-          points[1].y = m_first.y;
+          stroke[1].y = m_first.y;
         }
         // Snap at 26.565
         else if (angle < 36.0) {
-          points[1].x = m_first.x + SGN(dx)*maxsize;
-          points[1].y = m_first.y + SGN(dy)*maxsize/2;
+          stroke[1].x = m_first.x + SGN(dx)*maxsize;
+          stroke[1].y = m_first.y + SGN(dy)*maxsize/2;
         }
         // Snap at 45
         else if (angle < 54.0) {
-          points[1].x = m_first.x + SGN(dx)*minsize;
-          points[1].y = m_first.y + SGN(dy)*minsize;
+          stroke[1].x = m_first.x + SGN(dx)*minsize;
+          stroke[1].y = m_first.y + SGN(dy)*minsize;
         }
         // Snap at 63.435
         else if (angle < 72.0) {
-          points[1].x = m_first.x + SGN(dx)*maxsize/2;
-          points[1].y = m_first.y + SGN(dy)*maxsize;
+          stroke[1].x = m_first.x + SGN(dx)*maxsize/2;
+          stroke[1].y = m_first.y + SGN(dy)*maxsize;
         }
         // Snap vertically
         else {
-          points[1].x = m_first.x;
+          stroke[1].x = m_first.x;
         }
       }
       // Rectangles and ellipses
       else {
-        points[1].x = m_first.x + SGN(dx)*minsize;
-        points[1].y = m_first.y + SGN(dy)*minsize;
+        stroke[1].x = m_first.x + SGN(dx)*minsize;
+        stroke[1].y = m_first.y + SGN(dy)*minsize;
       }
     }
 
-    points[0] = m_first;
+    stroke[0] = m_first;
 
     if (m_fromCenter) {
-      int rx = points[1].x - m_first.x;
-      int ry = points[1].y - m_first.y;
-      points[0].x = m_first.x - rx;
-      points[0].y = m_first.y - ry;
-      points[1].x = m_first.x + rx;
-      points[1].y = m_first.y + ry;
+      int rx = stroke[1].x - m_first.x;
+      int ry = stroke[1].y - m_first.y;
+      stroke[0].x = m_first.x - rx;
+      stroke[0].y = m_first.y - ry;
+      stroke[1].x = m_first.x + rx;
+      stroke[1].y = m_first.y + ry;
     }
 
     // Adjust points for selection like tools (so we can select tiles)
     if (loop->getController()->canSnapToGrid() &&
         loop->getSnapToGrid() &&
         loop->getInk()->isSelection()) {
-      if (points[0].x < points[1].x)
-        points[1].x--;
-      else if (points[0].x > points[1].x)
-        points[0].x--;
-
-      if (points[0].y < points[1].y)
-        points[1].y--;
-      else if (points[0].y > points[1].y)
-        points[0].y--;
+      if (stroke[0].x < stroke[1].x)
+        stroke[1].x--;
+      else if (stroke[0].x > stroke[1].x)
+        stroke[0].x--;
+
+      if (stroke[0].y < stroke[1].y)
+        stroke[1].y--;
+      else if (stroke[0].y > stroke[1].y)
+        stroke[0].y--;
     }
   }
 
-  void getPointsToInterwine(const Points& input, Points& output) override {
+  void getStrokeToInterwine(const Stroke& input, Stroke& output) override {
     ASSERT(input.size() >= 2);
     if (input.size() < 2)
       return;
 
-    output.push_back(input[0]);
-    output.push_back(input[1]);
+    output.addPoint(input[0]);
+    output.addPoint(input[1]);
   }
 
-  void getStatusBarText(const Points& points, std::string& text) override {
-    ASSERT(points.size() >= 2);
-    if (points.size() < 2)
+  void getStatusBarText(const Stroke& stroke, std::string& text) override {
+    ASSERT(stroke.size() >= 2);
+    if (stroke.size() < 2)
       return;
 
     char buf[1024];
     sprintf(buf, "Start %3d %3d End %3d %3d (Size %3d %3d) Angle %.1f",
-            points[0].x, points[0].y,
-            points[1].x, points[1].y,
-            ABS(points[1].x-points[0].x)+1,
-            ABS(points[1].y-points[0].y)+1,
-            180.0 * std::atan2(static_cast<double>(points[0].y-points[1].y),
-                               static_cast<double>(points[1].x-points[0].x)) / PI);
+            stroke[0].x, stroke[0].y,
+            stroke[1].x, stroke[1].y,
+            ABS(stroke[1].x-stroke[0].x)+1,
+            ABS(stroke[1].y-stroke[0].y)+1,
+            180.0 * std::atan2(static_cast<double>(stroke[0].y-stroke[1].y),
+                               static_cast<double>(stroke[1].x-stroke[0].x)) / PI);
     text = buf;
   }
 
@@ -287,50 +287,51 @@ private:
 class PointByPointController : public MoveOriginCapability {
 public:
 
-  void pressButton(Points& points, const Point& point) override {
-    MoveOriginCapability::pressButton(points, point);
+  void pressButton(Stroke& stroke, const Point& point) override {
+    MoveOriginCapability::pressButton(stroke, point);
 
-    points.push_back(point);
-    points.push_back(point);
+    stroke.addPoint(point);
+    stroke.addPoint(point);
   }
 
-  bool releaseButton(Points& points, const Point& point) override {
-    ASSERT(!points.empty());
-    if (points.empty())
+  bool releaseButton(Stroke& stroke, const Point& point) override {
+    ASSERT(!stroke.empty());
+    if (stroke.empty())
       return false;
 
-    if (points[points.size()-2] == point &&
-        points[points.size()-1] == point)
+    if (stroke[stroke.size()-2] == point &&
+        stroke[stroke.size()-1] == point)
       return false;             // Click in the same point (no-drag), we are done
     else
       return true;              // Continue adding points
   }
 
-  void movement(ToolLoop* loop, Points& points, const Point& point) override {
-    ASSERT(!points.empty());
-    if (points.empty())
+  void movement(ToolLoop* loop, Stroke& stroke, const Point& point) override {
+    ASSERT(!stroke.empty());
+    if (stroke.empty())
       return;
 
-    if (MoveOriginCapability::isMovingOrigin(points, point))
+    if (MoveOriginCapability::isMovingOrigin(stroke, point))
       return;
 
-    points[points.size()-1] = point;
+    stroke[stroke.size()-1] = point;
   }
 
-  void getPointsToInterwine(const Points& input, Points& output) override {
+  void getStrokeToInterwine(const Stroke& input, Stroke& output) override {
     output = input;
   }
 
-  void getStatusBarText(const Points& points, std::string& text) override {
-    ASSERT(!points.empty());
-    if (points.empty())
+  void getStatusBarText(const Stroke& stroke, std::string& text) override {
+    ASSERT(!stroke.empty());
+    if (stroke.empty())
       return;
 
     char buf[1024];
     sprintf(buf, "Start %3d %3d End %3d %3d",
-            points[0].x, points[0].y,
-            points[points.size()-1].x,
-            points[points.size()-1].y);
+            stroke.firstPoint().x,
+            stroke.firstPoint().y,
+            stroke.lastPoint().x,
+            stroke.lastPoint().y);
     text = buf;
   }
 
@@ -342,30 +343,30 @@ public:
   bool canSnapToGrid() override { return false; }
   bool isOnePoint() override { return true; }
 
-  void pressButton(Points& points, const Point& point) override {
-    if (points.size() == 0)
-      points.push_back(point);
+  void pressButton(Stroke& stroke, const Point& point) override {
+    if (stroke.size() == 0)
+      stroke.addPoint(point);
   }
 
-  bool releaseButton(Points& points, const Point& point) override {
+  bool releaseButton(Stroke& stroke, const Point& point) override {
     return false;
   }
 
-  void movement(ToolLoop* loop, Points& points, const Point& point) override {
+  void movement(ToolLoop* loop, Stroke& stroke, const Point& point) override {
     // Do nothing
   }
 
-  void getPointsToInterwine(const Points& input, Points& output) override {
+  void getStrokeToInterwine(const Stroke& input, Stroke& output) override {
     output = input;
   }
 
-  void getStatusBarText(const Points& points, std::string& text) override {
-    ASSERT(!points.empty());
-    if (points.empty())
+  void getStatusBarText(const Stroke& stroke, std::string& text) override {
+    ASSERT(!stroke.empty());
+    if (stroke.empty())
       return;
 
     char buf[1024];
-    sprintf(buf, "Pos %3d %3d", points[0].x, points[0].y);
+    sprintf(buf, "Pos %3d %3d", stroke[0].x, stroke[0].y);
     text = buf;
   }
 
@@ -374,57 +375,57 @@ public:
 class FourPointsController : public MoveOriginCapability {
 public:
 
-  void pressButton(Points& points, const Point& point) override {
-    MoveOriginCapability::pressButton(points, point);
+  void pressButton(Stroke& stroke, const Point& point) override {
+    MoveOriginCapability::pressButton(stroke, point);
 
-    if (points.size() == 0) {
-      points.resize(4, point);
+    if (stroke.size() == 0) {
+      stroke.reset(4, point);
       m_clickCounter = 0;
     }
     else
       m_clickCounter++;
   }
 
-  bool releaseButton(Points& points, const Point& point) override {
+  bool releaseButton(Stroke& stroke, const Point& point) override {
     m_clickCounter++;
     return m_clickCounter < 4;
   }
 
-  void movement(ToolLoop* loop, Points& points, const Point& point) override {
-    if (MoveOriginCapability::isMovingOrigin(points, point))
+  void movement(ToolLoop* loop, Stroke& stroke, const Point& point) override {
+    if (MoveOriginCapability::isMovingOrigin(stroke, point))
       return;
 
     switch (m_clickCounter) {
       case 0:
-        for (size_t i=1; i<points.size(); ++i)
-          points[i] = point;
+        for (size_t i=1; i<stroke.size(); ++i)
+          stroke[i] = point;
         break;
       case 1:
       case 2:
-        points[1] = point;
-        points[2] = point;
+        stroke[1] = point;
+        stroke[2] = point;
         break;
       case 3:
-        points[2] = point;
+        stroke[2] = point;
         break;
     }
   }
 
-  void getPointsToInterwine(const Points& input, Points& output) override {
+  void getStrokeToInterwine(const Stroke& input, Stroke& output) override {
     output = input;
   }
 
-  void getStatusBarText(const Points& points, std::string& text) override {
-    ASSERT(points.size() >= 4);
-    if (points.size() < 4)
+  void getStatusBarText(const Stroke& stroke, std::string& text) override {
+    ASSERT(stroke.size() >= 4);
+    if (stroke.size() < 4)
       return;
 
     char buf[1024];
     sprintf(buf, "Start %3d %3d End %3d %3d (%3d %3d - %3d %3d)",
-            points[0].x, points[0].y,
-            points[3].x, points[3].y,
-            points[1].x, points[1].y,
-            points[2].x, points[2].y);
+            stroke[0].x, stroke[0].y,
+            stroke[3].x, stroke[3].y,
+            stroke[1].x, stroke[1].y,
+            stroke[2].x, stroke[2].y);
 
     text = buf;
   }
diff --git a/src/app/tools/intertwine.h b/src/app/tools/intertwine.h
index f480704..f73d8a2 100644
--- a/src/app/tools/intertwine.h
+++ b/src/app/tools/intertwine.h
@@ -15,6 +15,7 @@
 
 namespace app {
   namespace tools {
+    class Stroke;
     class ToolLoop;
 
     // Converts a sequence of points in several call to
@@ -28,8 +29,8 @@ namespace app {
       virtual ~Intertwine() { }
       virtual bool snapByAngle() { return false; }
       virtual void prepareIntertwine() { }
-      virtual void joinPoints(ToolLoop* loop, const Points& points) = 0;
-      virtual void fillPoints(ToolLoop* loop, const Points& points) = 0;
+      virtual void joinStroke(ToolLoop* loop, const Stroke& stroke) = 0;
+      virtual void fillStroke(ToolLoop* loop, const Stroke& stroke) = 0;
 
     protected:
       static void doPointshapePoint(int x, int y, ToolLoop* loop);
diff --git a/src/app/tools/intertwiners.h b/src/app/tools/intertwiners.h
index 8780dc3..0742a79 100644
--- a/src/app/tools/intertwiners.h
+++ b/src/app/tools/intertwiners.h
@@ -11,15 +11,13 @@ namespace tools {
 class IntertwineNone : public Intertwine {
 public:
 
-  void joinPoints(ToolLoop* loop, const Points& points) override
-  {
-    for (size_t c=0; c<points.size(); ++c)
-      doPointshapePoint(points[c].x, points[c].y, loop);
+  void joinStroke(ToolLoop* loop, const Stroke& stroke) override {
+    for (size_t c=0; c<stroke.size(); ++c)
+      doPointshapePoint(stroke[c].x, stroke[c].y, loop);
   }
 
-  void fillPoints(ToolLoop* loop, const Points& points) override
-  {
-    joinPoints(loop, points);
+  void fillStroke(ToolLoop* loop, const Stroke& stroke) override {
+    joinStroke(loop, stroke);
   }
 };
 
@@ -27,20 +25,20 @@ class IntertwineAsLines : public Intertwine {
 public:
   bool snapByAngle() override { return true; }
 
-  void joinPoints(ToolLoop* loop, const Points& points) override
+  void joinStroke(ToolLoop* loop, const Stroke& stroke) override
   {
-    if (points.size() == 0)
+    if (stroke.size() == 0)
       return;
 
-    if (points.size() == 1) {
-      doPointshapePoint(points[0].x, points[0].y, loop);
+    if (stroke.size() == 1) {
+      doPointshapePoint(stroke[0].x, stroke[0].y, loop);
     }
-    else if (points.size() >= 2) {
-      for (size_t c=0; c+1<points.size(); ++c) {
-        int x1 = points[c].x;
-        int y1 = points[c].y;
-        int x2 = points[c+1].x;
-        int y2 = points[c+1].y;
+    else if (stroke.size() >= 2) {
+      for (int c=0; c+1<stroke.size(); ++c) {
+        int x1 = stroke[c].x;
+        int y1 = stroke[c].y;
+        int x2 = stroke[c+1].x;
+        int y2 = stroke[c+1].y;
 
         algo_line(x1, y1, x2, y2, loop, (AlgoPixel)doPointshapePoint);
       }
@@ -48,44 +46,44 @@ public:
 
     // Closed shape (polygon outline)
     if (loop->getFilled()) {
-      algo_line(points[0].x, points[0].y,
-                points[points.size()-1].x,
-                points[points.size()-1].y, loop, (AlgoPixel)doPointshapePoint);
+      algo_line(stroke[0].x, stroke[0].y,
+                stroke[stroke.size()-1].x,
+                stroke[stroke.size()-1].y, loop, (AlgoPixel)doPointshapePoint);
     }
   }
 
-  void fillPoints(ToolLoop* loop, const Points& points) override
+  void fillStroke(ToolLoop* loop, const Stroke& stroke) override
   {
-    if (points.size() < 3) {
-      joinPoints(loop, points);
+    if (stroke.size() < 3) {
+      joinStroke(loop, stroke);
       return;
     }
 
     // Contour
-    joinPoints(loop, points);
+    joinStroke(loop, stroke);
 
     // Fill content
-    doc::algorithm::polygon(points.size(), (const int*)&points[0], loop, (AlgoHLine)doPointshapeHline);
+    doc::algorithm::polygon(stroke.size(), (const int*)&stroke[0], loop, (AlgoHLine)doPointshapeHline);
   }
 };
 
 class IntertwineAsRectangles : public Intertwine {
 public:
 
-  void joinPoints(ToolLoop* loop, const Points& points) override
+  void joinStroke(ToolLoop* loop, const Stroke& stroke) override
   {
-    if (points.size() == 0)
+    if (stroke.size() == 0)
       return;
 
-    if (points.size() == 1) {
-      doPointshapePoint(points[0].x, points[0].y, loop);
+    if (stroke.size() == 1) {
+      doPointshapePoint(stroke[0].x, stroke[0].y, loop);
     }
-    else if (points.size() >= 2) {
-      for (size_t c=0; c+1<points.size(); ++c) {
-        int x1 = points[c].x;
-        int y1 = points[c].y;
-        int x2 = points[c+1].x;
-        int y2 = points[c+1].y;
+    else if (stroke.size() >= 2) {
+      for (size_t c=0; c+1<stroke.size(); ++c) {
+        int x1 = stroke[c].x;
+        int y1 = stroke[c].y;
+        int x2 = stroke[c+1].x;
+        int y2 = stroke[c+1].y;
         int y;
 
         if (x1 > x2) std::swap(x1, x2);
@@ -102,18 +100,18 @@ public:
     }
   }
 
-  void fillPoints(ToolLoop* loop, const Points& points) override
+  void fillStroke(ToolLoop* loop, const Stroke& stroke) override
   {
-    if (points.size() < 2) {
-      joinPoints(loop, points);
+    if (stroke.size() < 2) {
+      joinStroke(loop, stroke);
       return;
     }
 
-    for (size_t c=0; c+1<points.size(); ++c) {
-      int x1 = points[c].x;
-      int y1 = points[c].y;
-      int x2 = points[c+1].x;
-      int y2 = points[c+1].y;
+    for (size_t c=0; c+1<stroke.size(); ++c) {
+      int x1 = stroke[c].x;
+      int y1 = stroke[c].y;
+      int x2 = stroke[c+1].x;
+      int y2 = stroke[c+1].y;
       int y;
 
       if (x1 > x2) std::swap(x1, x2);
@@ -128,20 +126,20 @@ public:
 class IntertwineAsEllipses : public Intertwine {
 public:
 
-  void joinPoints(ToolLoop* loop, const Points& points) override
+  void joinStroke(ToolLoop* loop, const Stroke& stroke) override
   {
-    if (points.size() == 0)
+    if (stroke.size() == 0)
       return;
 
-    if (points.size() == 1) {
-      doPointshapePoint(points[0].x, points[0].y, loop);
+    if (stroke.size() == 1) {
+      doPointshapePoint(stroke[0].x, stroke[0].y, loop);
     }
-    else if (points.size() >= 2) {
-      for (size_t c=0; c+1<points.size(); ++c) {
-        int x1 = points[c].x;
-        int y1 = points[c].y;
-        int x2 = points[c+1].x;
-        int y2 = points[c+1].y;
+    else if (stroke.size() >= 2) {
+      for (size_t c=0; c+1<stroke.size(); ++c) {
+        int x1 = stroke[c].x;
+        int y1 = stroke[c].y;
+        int x2 = stroke[c+1].x;
+        int y2 = stroke[c+1].y;
 
         if (x1 > x2) std::swap(x1, x2);
         if (y1 > y2) std::swap(y1, y2);
@@ -151,18 +149,18 @@ public:
     }
   }
 
-  void fillPoints(ToolLoop* loop, const Points& points) override
+  void fillStroke(ToolLoop* loop, const Stroke& stroke) override
   {
-    if (points.size() < 2) {
-      joinPoints(loop, points);
+    if (stroke.size() < 2) {
+      joinStroke(loop, stroke);
       return;
     }
 
-    for (size_t c=0; c+1<points.size(); ++c) {
-      int x1 = points[c].x;
-      int y1 = points[c].y;
-      int x2 = points[c+1].x;
-      int y2 = points[c+1].y;
+    for (size_t c=0; c+1<stroke.size(); ++c) {
+      int x1 = stroke[c].x;
+      int y1 = stroke[c].y;
+      int x2 = stroke[c+1].x;
+      int y2 = stroke[c+1].y;
 
       if (x1 > x2) std::swap(x1, x2);
       if (y1 > y2) std::swap(y1, y2);
@@ -175,79 +173,79 @@ public:
 class IntertwineAsBezier : public Intertwine {
 public:
 
-  void joinPoints(ToolLoop* loop, const Points& points) override
+  void joinStroke(ToolLoop* loop, const Stroke& stroke) override
   {
-    if (points.size() == 0)
+    if (stroke.size() == 0)
       return;
 
-    for (size_t c=0; c<points.size(); c += 4) {
-      if (points.size()-c == 1) {
-        doPointshapePoint(points[c].x, points[c].y, loop);
+    for (size_t c=0; c<stroke.size(); c += 4) {
+      if (stroke.size()-c == 1) {
+        doPointshapePoint(stroke[c].x, stroke[c].y, loop);
       }
-      else if (points.size()-c == 2) {
-        algo_line(points[c].x, points[c].y,
-                  points[c+1].x, points[c+1].y, loop, (AlgoPixel)doPointshapePoint);
+      else if (stroke.size()-c == 2) {
+        algo_line(stroke[c].x, stroke[c].y,
+                  stroke[c+1].x, stroke[c+1].y, loop, (AlgoPixel)doPointshapePoint);
       }
-      else if (points.size()-c == 3) {
-        algo_spline(points[c  ].x, points[c  ].y,
-                    points[c+1].x, points[c+1].y,
-                    points[c+1].x, points[c+1].y,
-                    points[c+2].x, points[c+2].y, loop, (AlgoLine)doPointshapeLine);
+      else if (stroke.size()-c == 3) {
+        algo_spline(stroke[c  ].x, stroke[c  ].y,
+                    stroke[c+1].x, stroke[c+1].y,
+                    stroke[c+1].x, stroke[c+1].y,
+                    stroke[c+2].x, stroke[c+2].y, loop, (AlgoLine)doPointshapeLine);
       }
       else {
-        algo_spline(points[c  ].x, points[c  ].y,
-                    points[c+1].x, points[c+1].y,
-                    points[c+2].x, points[c+2].y,
-                    points[c+3].x, points[c+3].y, loop, (AlgoLine)doPointshapeLine);
+        algo_spline(stroke[c  ].x, stroke[c  ].y,
+                    stroke[c+1].x, stroke[c+1].y,
+                    stroke[c+2].x, stroke[c+2].y,
+                    stroke[c+3].x, stroke[c+3].y, loop, (AlgoLine)doPointshapeLine);
       }
     }
   }
 
-  void fillPoints(ToolLoop* loop, const Points& points) override
+  void fillStroke(ToolLoop* loop, const Stroke& stroke) override
   {
-    joinPoints(loop, points);
+    joinStroke(loop, stroke);
   }
 };
 
 class IntertwineAsPixelPerfect : public Intertwine {
   struct PPData {
-    Points& pts;
+    Stroke& pts;
     ToolLoop* loop;
-    PPData(Points& pts, ToolLoop* loop) : pts(pts), loop(loop) { }
+    PPData(Stroke& pts, ToolLoop* loop) : pts(pts), loop(loop) { }
   };
 
   static void pixelPerfectLine(int x, int y, PPData* data)
   {
     gfx::Point newPoint(x, y);
 
-    if (data->pts.empty()
-      || data->pts[data->pts.size()-1] != newPoint) {
-      data->pts.push_back(newPoint);
+    if (data->pts.empty() ||
+        data->pts.lastPoint() != newPoint) {
+      data->pts.addPoint(newPoint);
     }
   }
 
-  Points m_pts;
+  Stroke m_pts;
 
 public:
   void prepareIntertwine() override {
-    m_pts.clear();
+    m_pts.reset();
   }
 
-  void joinPoints(ToolLoop* loop, const Points& points) override {
-    if (points.size() == 0)
+  void joinStroke(ToolLoop* loop, const Stroke& stroke) override {
+    if (stroke.size() == 0)
       return;
-    else if (m_pts.empty() && points.size() == 1) {
-      m_pts = points;
+    else if (m_pts.empty() && stroke.size() == 1) {
+      m_pts = stroke;
     }
     else {
       PPData data(m_pts, loop);
 
-      for (size_t c=0; c+1<points.size(); ++c) {
+      for (size_t c=0; c+1<stroke.size(); ++c) {
         algo_line(
-          points[c].x,
-          points[c].y,
-          points[c+1].x,
-          points[c+1].y,
+          stroke[c].x,
+          stroke[c].y,
+          stroke[c+1].x,
+          stroke[c+1].y,
           (void*)&data,
           (AlgoPixel)&IntertwineAsPixelPerfect::pixelPerfectLine);
       }
@@ -268,18 +266,18 @@ public:
     }
   }
 
-  void fillPoints(ToolLoop* loop, const Points& points) override
+  void fillStroke(ToolLoop* loop, const Stroke& stroke) override
   {
-    if (points.size() < 3) {
-      joinPoints(loop, points);
+    if (stroke.size() < 3) {
+      joinStroke(loop, stroke);
       return;
     }
 
     // Contour
-    joinPoints(loop, points);
+    joinStroke(loop, stroke);
 
     // Fill content
-    doc::algorithm::polygon(points.size(), (const int*)&points[0], loop, (AlgoHLine)doPointshapeHline);
+    doc::algorithm::polygon(stroke.size(), (const int*)&stroke[0], loop, (AlgoHLine)doPointshapeHline);
   }
 };
 
diff --git a/src/app/tools/stroke.cpp b/src/app/tools/stroke.cpp
new file mode 100644
index 0000000..fdf3b75
--- /dev/null
+++ b/src/app/tools/stroke.cpp
@@ -0,0 +1,62 @@
+// Aseprite
+// Copyright (C) 2001-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.
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "app/tools/stroke.h"
+
+namespace app {
+namespace tools {
+
+void Stroke::reset()
+{
+  m_points.clear();
+}
+
+void Stroke::reset(int n, const gfx::Point& point)
+{
+  m_points.resize(n, point);
+}
+
+void Stroke::addPoint(const gfx::Point& point)
+{
+  m_points.push_back(point);
+}
+
+void Stroke::offset(const gfx::Point& delta)
+{
+  for (auto& p : m_points)
+    p += delta;
+}
+
+gfx::Rect Stroke::bounds() const
+{
+  if (m_points.empty())
+    return gfx::Rect();
+
+  gfx::Point
+    minpt(m_points[0]),
+    maxpt(m_points[0]);
+
+  for (size_t c=1; c<m_points.size(); ++c) {
+    int x = m_points[c].x;
+    int y = m_points[c].y;
+    if (minpt.x > x) minpt.x = x;
+    if (minpt.y > y) minpt.y = y;
+    if (maxpt.x < x) maxpt.x = x;
+    if (maxpt.y < y) maxpt.y = y;
+  }
+
+  return gfx::Rect(minpt.x, minpt.y,
+                   maxpt.x - minpt.x + 1,
+                   maxpt.y - minpt.y + 1);
+}
+
+} // namespace tools
+} // namespace app
diff --git a/src/app/tools/stroke.h b/src/app/tools/stroke.h
new file mode 100644
index 0000000..618ae0f
--- /dev/null
+++ b/src/app/tools/stroke.h
@@ -0,0 +1,66 @@
+// Aseprite
+// Copyright (C) 2001-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_STROKE_H_INCLUDED
+#define APP_TOOLS_STROKE_H_INCLUDED
+#pragma once
+
+#include "gfx/point.h"
+#include "gfx/rect.h"
+
+#include <vector>
+
+namespace app {
+  namespace tools {
+
+    class Stroke {
+    public:
+      typedef std::vector<gfx::Point> Points;
+      typedef Points::const_iterator const_iterator;
+
+      const_iterator begin() const { return m_points.begin(); }
+      const_iterator end() const { return m_points.end(); }
+
+      bool empty() const { return m_points.empty(); }
+      int size() const { return (int)m_points.size(); }
+
+      const gfx::Point& operator[](int i) const { return m_points[i]; }
+      gfx::Point& operator[](int i) { return m_points[i]; }
+
+      const gfx::Point& firstPoint() const {
+        ASSERT(!m_points.empty());
+        return m_points[0];
+      }
+
+      const gfx::Point& lastPoint() const {
+        ASSERT(!m_points.empty());
+        return m_points[m_points.size()-1];
+      }
+
+      // Clears the whole stroke.
+      void reset();
+
+      // Reset the stroke as "n" points in the given "point" position.
+      void reset(int n, const gfx::Point& point);
+
+      // Adds a new point to the stroke.
+      void addPoint(const gfx::Point& point);
+
+      // Displaces all X,Y coordinates the given delta.
+      void offset(const gfx::Point& delta);
+
+      // Returns the bounds of the stroke (minimum/maximum position).
+      gfx::Rect bounds() const;
+
+    public:
+      Points m_points;
+    };
+
+  } // namespace tools
+} // namespace app
+
+#endif
diff --git a/src/app/tools/symmetries.cpp b/src/app/tools/symmetries.cpp
new file mode 100644
index 0000000..6f34f79
--- /dev/null
+++ b/src/app/tools/symmetries.cpp
@@ -0,0 +1,40 @@
+// 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.
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "app/tools/symmetries.h"
+
+#include "app/tools/stroke.h"
+
+namespace app {
+namespace tools {
+
+void HorizontalSymmetry::generateStrokes(const Stroke& mainStroke, Strokes& strokes)
+{
+  strokes.push_back(mainStroke);
+
+  Stroke stroke2;
+  for (const auto& pt : mainStroke)
+    stroke2.addPoint(gfx::Point(m_x - (pt.x - m_x + 1), pt.y));
+  strokes.push_back(stroke2);
+}
+
+void VerticalSymmetry::generateStrokes(const Stroke& mainStroke, Strokes& strokes)
+{
+  strokes.push_back(mainStroke);
+
+  Stroke stroke2;
+  for (const auto& pt : mainStroke)
+    stroke2.addPoint(gfx::Point(pt.x, m_y - (pt.y - m_y + 1)));
+  strokes.push_back(stroke2);
+}
+
+} // namespace tools
+} // namespace app
diff --git a/src/app/tools/symmetries.h b/src/app/tools/symmetries.h
new file mode 100644
index 0000000..388e6e1
--- /dev/null
+++ b/src/app/tools/symmetries.h
@@ -0,0 +1,37 @@
+// 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) override;
+private:
+  int m_x;
+};
+
+class VerticalSymmetry : public Symmetry {
+public:
+  VerticalSymmetry(int y) : m_y(y) { }
+  void generateStrokes(const Stroke& mainStroke, Strokes& strokes) override;
+private:
+  int m_y;
+};
+
+} // namespace tools
+} // namespace app
+
+#endif
diff --git a/src/app/tools/symmetry.h b/src/app/tools/symmetry.h
new file mode 100644
index 0000000..84faae6
--- /dev/null
+++ b/src/app/tools/symmetry.h
@@ -0,0 +1,31 @@
+// 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_SYMMETRY_H_INCLUDED
+#define APP_TOOLS_SYMMETRY_H_INCLUDED
+#pragma once
+
+#include "app/tools/stroke.h"
+
+#include <vector>
+
+namespace app {
+  namespace tools {
+
+    typedef std::vector<Stroke> Strokes;
+
+    // This class controls user input.
+    class Symmetry {
+    public:
+      virtual ~Symmetry() { }
+      virtual void generateStrokes(const Stroke& stroke, Strokes& strokes) = 0;
+    };
+
+  } // namespace tools
+} // namespace app
+
+#endif
diff --git a/src/app/tools/tool_box.cpp b/src/app/tools/tool_box.cpp
index 2af9585..1844b6a 100644
--- a/src/app/tools/tool_box.cpp
+++ b/src/app/tools/tool_box.cpp
@@ -16,6 +16,7 @@
 #include "app/tools/ink.h"
 #include "app/tools/intertwine.h"
 #include "app/tools/point_shape.h"
+#include "app/tools/stroke.h"
 #include "app/tools/tool_group.h"
 #include "app/tools/tool_loop.h"
 #include "base/bind.h"
diff --git a/src/app/tools/tool_loop.h b/src/app/tools/tool_loop.h
index 51cb148..9e8306f 100644
--- a/src/app/tools/tool_loop.h
+++ b/src/app/tools/tool_loop.h
@@ -43,6 +43,7 @@ namespace app {
     class Ink;
     class Intertwine;
     class PointShape;
+    class Symmetry;
     class Tool;
 
     using namespace doc;
@@ -202,6 +203,7 @@ namespace app {
       virtual PointShape* getPointShape() = 0;
       virtual Intertwine* getIntertwine() = 0;
       virtual TracePolicy getTracePolicy() = 0;
+      virtual Symmetry* getSymmetry() = 0;
 
       virtual const doc::Remap* getShadingRemap() = 0;
 
diff --git a/src/app/tools/tool_loop_manager.cpp b/src/app/tools/tool_loop_manager.cpp
index f700225..eccb1bc 100644
--- a/src/app/tools/tool_loop_manager.cpp
+++ b/src/app/tools/tool_loop_manager.cpp
@@ -17,12 +17,15 @@
 #include "app/tools/ink.h"
 #include "app/tools/intertwine.h"
 #include "app/tools/point_shape.h"
+#include "app/tools/symmetry.h"
 #include "app/tools/tool_loop.h"
 #include "doc/image.h"
 #include "doc/primitives.h"
 #include "doc/sprite.h"
 #include "gfx/region.h"
 
+#include <climits>
+
 namespace app {
 namespace tools {
 
@@ -49,7 +52,7 @@ void ToolLoopManager::prepareLoop(const Pointer& pointer,
                                   ui::KeyModifiers modifiers)
 {
   // Start with no points at all
-  m_points.clear();
+  m_stroke.reset();
 
   // Prepare the ink
   m_toolLoop->getInk()->prepareInk(m_toolLoop);
@@ -106,10 +109,10 @@ void ToolLoopManager::pressButton(const Pointer& pointer)
   m_oldPoint = spritePoint;
   snapToGrid(spritePoint);
 
-  m_toolLoop->getController()->pressButton(m_points, spritePoint);
+  m_toolLoop->getController()->pressButton(m_stroke, spritePoint);
 
   std::string statusText;
-  m_toolLoop->getController()->getStatusBarText(m_points, statusText);
+  m_toolLoop->getController()->getStatusBarText(m_stroke, statusText);
   m_toolLoop->updateStatusBar(statusText.c_str());
 
   doLoopStep(false);
@@ -125,7 +128,7 @@ bool ToolLoopManager::releaseButton(const Pointer& pointer)
   Point spritePoint = pointer.point();
   snapToGrid(spritePoint);
 
-  bool res = m_toolLoop->getController()->releaseButton(m_points, spritePoint);
+  bool res = m_toolLoop->getController()->releaseButton(m_stroke, spritePoint);
 
   if (!res && (m_toolLoop->getInk()->isSelection() || m_toolLoop->getFilled())) {
     m_toolLoop->getInk()->setFinalStep(m_toolLoop, true);
@@ -150,10 +153,10 @@ void ToolLoopManager::movement(const Pointer& pointer)
   m_oldPoint = spritePoint;
   snapToGrid(spritePoint);
 
-  m_toolLoop->getController()->movement(m_toolLoop, m_points, spritePoint);
+  m_toolLoop->getController()->movement(m_toolLoop, m_stroke, spritePoint);
 
   std::string statusText;
-  m_toolLoop->getController()->getStatusBarText(m_points, statusText);
+  m_toolLoop->getController()->getStatusBarText(m_stroke, statusText);
   m_toolLoop->updateStatusBar(statusText.c_str());
 
   doLoopStep(false);
@@ -161,18 +164,28 @@ void ToolLoopManager::movement(const Pointer& pointer)
 
 void ToolLoopManager::doLoopStep(bool last_step)
 {
-  Points points_to_interwine;
+  // Original set of points to interwine (original user stroke).
+  Stroke main_stroke;
   if (!last_step)
-    m_toolLoop->getController()->getPointsToInterwine(m_points, points_to_interwine);
+    m_toolLoop->getController()->getStrokeToInterwine(m_stroke, main_stroke);
   else
-    points_to_interwine = m_points;
-
-  Point offset(m_toolLoop->getOffset());
-  for (size_t i=0; i<points_to_interwine.size(); ++i)
-    points_to_interwine[i] += offset;
+    main_stroke = m_stroke;
+  main_stroke.offset(m_toolLoop->getOffset());
+
+  // Apply symmetry
+  Symmetry* symmetry = m_toolLoop->getSymmetry();
+  Strokes strokes;
+  if (symmetry)
+    symmetry->generateStrokes(main_stroke, strokes);
+  else
+    strokes.push_back(main_stroke);
 
   // Calculate the area to be updated in all document observers.
-  calculateDirtyArea(points_to_interwine);
+  gfx::Rect strokeBounds;
+  for (const Stroke& stroke : strokes)
+    strokeBounds |= stroke.bounds();
+
+  calculateDirtyArea(strokeBounds);
 
   // Validate source image area.
   if (m_toolLoop->getInk()->needsSpecialSourceArea()) {
@@ -201,10 +214,12 @@ void ToolLoopManager::doLoopStep(bool last_step)
   m_toolLoop->validateDstImage(m_dirtyArea);
 
   // Get the modified area in the sprite with this intertwined set of points
-  if (!m_toolLoop->getFilled() || (!last_step && !m_toolLoop->getPreviewFilled()))
-    m_toolLoop->getIntertwine()->joinPoints(m_toolLoop, points_to_interwine);
-  else
-    m_toolLoop->getIntertwine()->fillPoints(m_toolLoop, points_to_interwine);
+  for (const Stroke& stroke : strokes) {
+    if (!m_toolLoop->getFilled() || (!last_step && !m_toolLoop->getPreviewFilled()))
+      m_toolLoop->getIntertwine()->joinStroke(m_toolLoop, stroke);
+    else
+      m_toolLoop->getIntertwine()->fillStroke(m_toolLoop, stroke);
+  }
 
   if (m_toolLoop->getTracePolicy() == TracePolicy::Overlap) {
     // Copy destination to source (yes, destination to source). In
@@ -226,7 +241,7 @@ void ToolLoopManager::snapToGrid(Point& point)
   point = snap_to_grid(m_toolLoop->getGridBounds(), point);
 }
 
-void ToolLoopManager::calculateDirtyArea(const Points& points)
+void ToolLoopManager::calculateDirtyArea(const gfx::Rect& strokeBounds)
 {
   // Save the current dirty area if it's needed
   Region prevDirtyArea;
@@ -236,14 +251,19 @@ void ToolLoopManager::calculateDirtyArea(const Points& points)
   // Start with a fresh dirty area
   m_dirtyArea.clear();
 
-  if (points.size() > 0) {
-    Point minpt, maxpt;
-    calculateMinMax(points, minpt, maxpt);
-
+  if (!strokeBounds.isEmpty()) {
     // Expand the dirty-area with the pen width
     Rect r1, r2;
-    m_toolLoop->getPointShape()->getModifiedArea(m_toolLoop, minpt.x, minpt.y, r1);
-    m_toolLoop->getPointShape()->getModifiedArea(m_toolLoop, maxpt.x, maxpt.y, r2);
+
+    m_toolLoop->getPointShape()->getModifiedArea(
+      m_toolLoop,
+      strokeBounds.x,
+      strokeBounds.y, r1);
+
+    m_toolLoop->getPointShape()->getModifiedArea(
+      m_toolLoop,
+      strokeBounds.x+strokeBounds.w-1,
+      strokeBounds.y+strokeBounds.h-1, r2);
 
     m_dirtyArea.createUnion(m_dirtyArea, Region(r1.createUnion(r2)));
   }
@@ -300,22 +320,5 @@ void ToolLoopManager::calculateDirtyArea(const Points& points)
   }
 }
 
-void ToolLoopManager::calculateMinMax(const Points& points, Point& minpt, Point& maxpt)
-{
-  ASSERT(points.size() > 0);
-
-  minpt.x = points[0].x;
-  minpt.y = points[0].y;
-  maxpt.x = points[0].x;
-  maxpt.y = points[0].y;
-
-  for (size_t c=1; c<points.size(); ++c) {
-    minpt.x = MIN(minpt.x, points[c].x);
-    minpt.y = MIN(minpt.y, points[c].y);
-    maxpt.x = MAX(maxpt.x, points[c].x);
-    maxpt.y = MAX(maxpt.y, points[c].y);
-  }
-}
-
 } // namespace tools
 } // namespace app
diff --git a/src/app/tools/tool_loop_manager.h b/src/app/tools/tool_loop_manager.h
index e6efdcb..a3462e2 100644
--- a/src/app/tools/tool_loop_manager.h
+++ b/src/app/tools/tool_loop_manager.h
@@ -9,6 +9,7 @@
 #define APP_TOOLS_TOOL_LOOP_MANAGER_H_INCLUDED
 #pragma once
 
+#include "app/tools/stroke.h"
 #include "gfx/point.h"
 #include "gfx/region.h"
 #include "ui/keys.h"
@@ -86,18 +87,13 @@ namespace app {
       void movement(const Pointer& pointer);
 
     private:
-      typedef std::vector<gfx::Point> Points;
-
       void doLoopStep(bool last_step);
       void snapToGrid(gfx::Point& point);
 
-      void calculateDirtyArea(const Points& points);
-      void calculateMinMax(const Points& points,
-        gfx::Point& minpt,
-        gfx::Point& maxpt);
+      void calculateDirtyArea(const gfx::Rect& strokeBounds);
 
       ToolLoop* m_toolLoop;
-      Points m_points;
+      Stroke m_stroke;
       Pointer m_lastPointer;
       gfx::Point m_oldPoint;
       gfx::Region& m_dirtyArea;
@@ -106,4 +102,4 @@ namespace app {
   } // namespace tools
 } // namespace app
 
-#endif  // TOOLS_TOOL_H_INCLUDED
+#endif
diff --git a/src/app/ui/context_bar.cpp b/src/app/ui/context_bar.cpp
index 15a2b5c..daeee2c 100644
--- a/src/app/ui/context_bar.cpp
+++ b/src/app/ui/context_bar.cpp
@@ -13,6 +13,7 @@
 
 #include "app/app.h"
 #include "app/commands/commands.h"
+#include "app/document.h"
 #include "app/modules/gfx.h"
 #include "app/modules/gui.h"
 #include "app/modules/palettes.h"
@@ -1116,6 +1117,49 @@ protected:
   }
 };
 
+class ContextBar::SymmetryField : public ButtonSet {
+public:
+  SymmetryField() : ButtonSet(3) {
+    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);
+  }
+
+  void updateWithCurrentDocument() {
+    Document* doc = UIContext::instance()->activeDocument();
+    if (!doc)
+      return;
+
+    DocumentPreferences& docPref = Preferences::instance().document(doc);
+
+    setSelectedItem((int)docPref.symmetry.mode());
+  }
+
+private:
+  void onItemChange(Item* item) override {
+    ButtonSet::onItemChange(item);
+
+    Document* doc = UIContext::instance()->activeDocument();
+    if (!doc)
+      return;
+
+    DocumentPreferences& docPref =
+      Preferences::instance().document(doc);
+
+    docPref.symmetry.mode((app::gen::SymmetryMode)selectedItem());
+
+    // Redraw symmetry rules
+    doc->notifyGeneralUpdate();
+  }
+};
+
 ContextBar::ContextBar()
   : Box(HORIZONTAL)
 {
@@ -1175,6 +1219,9 @@ ContextBar::ContextBar()
   setup_mini_font(m_toleranceLabel);
   setup_mini_font(m_inkOpacityLabel);
 
+  addChild(m_symmetry = new SymmetryField());
+  m_symmetry->setVisible(Preferences::instance().symmetryMode.enabled());
+
   TooltipManager* tooltipManager = new TooltipManager();
   addChild(tooltipManager);
 
@@ -1195,10 +1242,14 @@ ContextBar::ContextBar()
   m_selectionMode->setupTooltips(tooltipManager);
   m_dropPixels->setupTooltips(tooltipManager);
   m_freehandAlgo->setupTooltips(tooltipManager);
+  m_symmetry->setupTooltips(tooltipManager);
 
   Preferences::instance().toolBox.activeTool.AfterChange.connect(
     Bind<void>(&ContextBar::onCurrentToolChange, this));
 
+  Preferences::instance().symmetryMode.enabled.AfterChange.connect(
+    Bind<void>(&ContextBar::onSymmetryModeChange, this));
+
   m_dropPixels->DropPixels.connect(&ContextBar::onDropPixels, this);
 
   setActiveBrush(createBrushFromPreferences());
@@ -1240,6 +1291,11 @@ void ContextBar::onCurrentToolChange()
   }
 }
 
+void ContextBar::onSymmetryModeChange()
+{
+  updateForCurrentTool();
+}
+
 void ContextBar::onDropPixels(ContextBarObserver::DropAction action)
 {
   notifyObservers(&ContextBarObserver::onDropPixels, action);
@@ -1394,6 +1450,8 @@ void ContextBar::updateForTool(tools::Tool* tool)
   m_pivot->setVisible(true);
   m_dropPixels->setVisible(false);
   m_selectBoxHelp->setVisible(false);
+  m_symmetry->setVisible(Preferences::instance().symmetryMode.enabled());
+  m_symmetry->updateWithCurrentDocument();
 
   layout();
 }
diff --git a/src/app/ui/context_bar.h b/src/app/ui/context_bar.h
index 83ce9c7..1fa8cfa 100644
--- a/src/app/ui/context_bar.h
+++ b/src/app/ui/context_bar.h
@@ -83,6 +83,7 @@ namespace app {
     void onBrushSizeChange();
     void onBrushAngleChange();
     void onCurrentToolChange();
+    void onSymmetryModeChange();
     void onDropPixels(ContextBarObserver::DropAction action);
 
     struct BrushSlot {
@@ -120,6 +121,7 @@ namespace app {
     class EyedropperField;
     class DropPixelsField;
     class AutoSelectLayerField;
+    class SymmetryField;
 
     BrushTypeField* m_brushType;
     BrushAngleField* m_brushAngle;
@@ -150,6 +152,7 @@ namespace app {
     doc::BrushRef m_activeBrush;
     BrushSlots m_brushes;
     ui::Label* m_selectBoxHelp;
+    SymmetryField* m_symmetry;
     ScopedConnection m_sizeConn;
     ScopedConnection m_angleConn;
     ScopedConnection m_opacityConn;
diff --git a/src/app/ui/editor/editor.cpp b/src/app/ui/editor/editor.cpp
index b8a5236..d9e8b65 100644
--- a/src/app/ui/editor/editor.cpp
+++ b/src/app/ui/editor/editor.cpp
@@ -632,6 +632,37 @@ void Editor::drawSpriteUnclippedRect(ui::Graphics* g, const gfx::Rect& _rc)
     }
   }
 
+  // Symmetry mode
+  {
+    switch (docPref.symmetry.mode()) {
+      case app::gen::SymmetryMode::NONE:
+        // Do nothing
+        break;
+      case app::gen::SymmetryMode::HORIZONTAL: {
+        int x = docPref.symmetry.xAxis();
+        if (x > 0) {
+          gfx::Color color = color_utils::color_for_ui(docPref.grid.color());
+          g->drawVLine(color,
+                       enclosingRect.x + m_zoom.apply(x),
+                       enclosingRect.y,
+                       enclosingRect.h);
+        }
+        break;
+      }
+      case app::gen::SymmetryMode::VERTICAL: {
+        int y = docPref.symmetry.yAxis();
+        if (y > 0) {
+          gfx::Color color = color_utils::color_for_ui(docPref.grid.color());
+          g->drawHLine(color,
+                       enclosingRect.x,
+                       enclosingRect.y + m_zoom.apply(y),
+                       enclosingRect.w);
+        }
+        break;
+      }
+    }
+  }
+
   if (m_flags & kShowOutside) {
     // Draw the borders that enclose the sprite.
     enclosingRect.enlarge(1);
diff --git a/src/app/ui/editor/tool_loop_impl.cpp b/src/app/ui/editor/tool_loop_impl.cpp
index 27094e4..638714a 100644
--- a/src/app/ui/editor/tool_loop_impl.cpp
+++ b/src/app/ui/editor/tool_loop_impl.cpp
@@ -26,6 +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/tool.h"
 #include "app/tools/tool_box.h"
 #include "app/tools/tool_loop.h"
@@ -77,6 +78,7 @@ protected:
   tools::PointShape* m_pointShape;
   tools::Intertwine* m_intertwine;
   tools::TracePolicy m_tracePolicy;
+  base::UniquePtr<tools::Symmetry> m_symmetry;
   base::UniquePtr<doc::Remap> m_shadingRemap;
   doc::color_t m_fgColor;
   doc::color_t m_bgColor;
@@ -110,6 +112,7 @@ public:
     , m_pointShape(m_tool->getPointShape(m_button))
     , m_intertwine(m_tool->getIntertwine(m_button))
     , m_tracePolicy(m_tool->getTracePolicy(m_button))
+    , m_symmetry(nullptr)
     , m_fgColor(color_utils::color_for_target_mask(fgColor, ColorTarget(m_layer)))
     , m_bgColor(color_utils::color_for_target_mask(bgColor, ColorTarget(m_layer)))
     , m_primaryColor(button == tools::ToolLoop::Left ? m_fgColor: m_bgColor)
@@ -137,6 +140,28 @@ public:
       }
     }
 
+    // Symmetry mode
+    switch (m_docPref.symmetry.mode()) {
+
+      case app::gen::SymmetryMode::NONE:
+        ASSERT(m_symmetry == nullptr);
+        break;
+
+      case app::gen::SymmetryMode::HORIZONTAL:
+        if (m_docPref.symmetry.xAxis() == 0)
+          m_docPref.symmetry.xAxis(m_sprite->width()/2);
+
+        m_symmetry.reset(new app::tools::HorizontalSymmetry(m_docPref.symmetry.xAxis()));
+        break;
+
+      case app::gen::SymmetryMode::VERTICAL:
+        if (m_docPref.symmetry.yAxis() == 0)
+          m_docPref.symmetry.yAxis(m_sprite->height()/2);
+
+        m_symmetry.reset(new app::tools::VerticalSymmetry(m_docPref.symmetry.yAxis()));
+        break;
+    }
+
     // Ignore opacity for these inks
     if (!tools::inkHasOpacity(m_toolPref.ink()) &&
         m_brush->type() != kImageBrushType &&
@@ -194,6 +219,7 @@ public:
   tools::PointShape* getPointShape() override { return m_pointShape; }
   tools::Intertwine* getIntertwine() override { return m_intertwine; }
   tools::TracePolicy getTracePolicy() override { return m_tracePolicy; }
+  tools::Symmetry* getSymmetry() override { return m_symmetry.get(); }
   doc::Remap* getShadingRemap() override { return m_shadingRemap; }
 
   gfx::Region& getDirtyArea() override {
diff --git a/src/app/ui_context.cpp b/src/app/ui_context.cpp
index bb54022..527bda3 100644
--- a/src/app/ui_context.cpp
+++ b/src/app/ui_context.cpp
@@ -63,14 +63,21 @@ bool UIContext::isUIAvailable() const
 DocumentView* UIContext::activeView() const
 {
   if (!isUIAvailable())
-    return NULL;
+    return nullptr;
+
+  MainWindow* mainWindow = App::instance()->getMainWindow();
+  if (!mainWindow)
+    return nullptr;
+
+  Workspace* workspace = mainWindow->getWorkspace();
+  if (!workspace)
+    return nullptr;
 
-  Workspace* workspace = App::instance()->getMainWindow()->getWorkspace();
   WorkspaceView* view = workspace->activeView();
   if (DocumentView* docView = dynamic_cast<DocumentView*>(view))
     return docView;
   else
-    return NULL;
+    return nullptr;
 }
 
 void UIContext::setActiveView(DocumentView* docView)

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-games/aseprite.git



More information about the Pkg-games-commits mailing list