Explorar o código

Encapsulated shapes in DrawToolShape in DrawingTool. Selected shapes are now in list.

jpcaram %!s(int64=11) %!d(string=hai) anos
pai
achega
9bfa13a5b6
Modificáronse 3 ficheiros con 231 adicións e 97 borrados
  1. 182 97
      FlatCAMDraw.py
  2. 45 0
      doc/source/planning.rst
  3. 4 0
      manual/editor.rst

+ 182 - 97
FlatCAMDraw.py

@@ -19,6 +19,22 @@ from mpl_toolkits.axes_grid.anchored_artists import AnchoredDrawingArea
 from rtree import index as rtindex
 from rtree import index as rtindex
 
 
 
 
+class DrawToolShape(object):
+
+    def __init__(self, geo=[]):
+
+        # Shapely type or list of such
+        self.geo = geo
+        self.utility = False
+
+
+class DrawToolUtilityShape(DrawToolShape):
+
+    def __init__(self, geo=[]):
+        super(DrawToolUtilityShape, self).__init__(geo=geo)
+        self.utility = True
+
+
 class DrawTool(object):
 class DrawTool(object):
     """
     """
     Abstract Class representing a tool in the drawing
     Abstract Class representing a tool in the drawing
@@ -31,7 +47,7 @@ class DrawTool(object):
         self.complete = False
         self.complete = False
         self.start_msg = "Click on 1st point..."
         self.start_msg = "Click on 1st point..."
         self.points = []
         self.points = []
-        self.geometry = None
+        self.geometry = None  # DrawToolShape or None
 
 
     def click(self, point):
     def click(self, point):
         """
         """
@@ -80,7 +96,7 @@ class FCCircle(FCShapeTool):
             p1 = self.points[0]
             p1 = self.points[0]
             p2 = data
             p2 = data
             radius = sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2)
             radius = sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2)
-            return Point(p1).buffer(radius)
+            return DrawToolUtilityShape(Point(p1).buffer(radius))
 
 
         return None
         return None
 
 
@@ -88,7 +104,7 @@ class FCCircle(FCShapeTool):
         p1 = self.points[0]
         p1 = self.points[0]
         p2 = self.points[1]
         p2 = self.points[1]
         radius = distance(p1, p2)
         radius = distance(p1, p2)
-        self.geometry = Point(p1).buffer(radius)
+        self.geometry = DrawToolShape(Point(p1).buffer(radius))
         self.complete = True
         self.complete = True
 
 
 
 
@@ -144,7 +160,7 @@ class FCArc(FCShapeTool):
             center = self.points[0]
             center = self.points[0]
             p1 = data
             p1 = data
 
 
-            return LineString([center, p1])
+            return DrawToolUtilityShape(LineString([center, p1]))
 
 
         if len(self.points) == 2:  # Show the arc
         if len(self.points) == 2:  # Show the arc
 
 
@@ -157,9 +173,9 @@ class FCArc(FCShapeTool):
                 startangle = arctan2(p1[1] - center[1], p1[0] - center[0])
                 startangle = arctan2(p1[1] - center[1], p1[0] - center[0])
                 stopangle = arctan2(p2[1] - center[1], p2[0] - center[0])
                 stopangle = arctan2(p2[1] - center[1], p2[0] - center[0])
 
 
-                return [LineString(arc(center, radius, startangle, stopangle,
+                return DrawToolUtilityShape([LineString(arc(center, radius, startangle, stopangle,
                                        self.direction, self.steps_per_circ)),
                                        self.direction, self.steps_per_circ)),
-                        Point(center)]
+                        Point(center)])
 
 
             elif self.mode == '132':
             elif self.mode == '132':
                 p1 = array(self.points[0])
                 p1 = array(self.points[0])
@@ -172,9 +188,9 @@ class FCArc(FCShapeTool):
                 startangle = arctan2(p1[1] - center[1], p1[0] - center[0])
                 startangle = arctan2(p1[1] - center[1], p1[0] - center[0])
                 stopangle = arctan2(p3[1] - center[1], p3[0] - center[0])
                 stopangle = arctan2(p3[1] - center[1], p3[0] - center[0])
 
 
-                return [LineString(arc(center, radius, startangle, stopangle,
+                return DrawToolUtilityShape([LineString(arc(center, radius, startangle, stopangle,
                                    direction, self.steps_per_circ)),
                                    direction, self.steps_per_circ)),
-                        Point(center), Point(p1), Point(p3)]
+                        Point(center), Point(p1), Point(p3)])
 
 
             else:  # '12c'
             else:  # '12c'
                 p1 = array(self.points[0])
                 p1 = array(self.points[0])
@@ -205,9 +221,9 @@ class FCArc(FCShapeTool):
                 startangle = arctan2(p1[1] - center[1], p1[0] - center[0])
                 startangle = arctan2(p1[1] - center[1], p1[0] - center[0])
                 stopangle = arctan2(p2[1] - center[1], p2[0] - center[0])
                 stopangle = arctan2(p2[1] - center[1], p2[0] - center[0])
 
 
-                return [LineString(arc(center, radius, startangle, stopangle,
+                return DrawToolUtilityShape([LineString(arc(center, radius, startangle, stopangle,
                                        self.direction, self.steps_per_circ)),
                                        self.direction, self.steps_per_circ)),
-                        Point(center)]
+                        Point(center)])
 
 
         return None
         return None
 
 
@@ -221,8 +237,8 @@ class FCArc(FCShapeTool):
             radius = distance(center, p1)
             radius = distance(center, p1)
             startangle = arctan2(p1[1] - center[1], p1[0] - center[0])
             startangle = arctan2(p1[1] - center[1], p1[0] - center[0])
             stopangle = arctan2(p2[1] - center[1], p2[0] - center[0])
             stopangle = arctan2(p2[1] - center[1], p2[0] - center[0])
-            self.geometry = LineString(arc(center, radius, startangle, stopangle,
-                                           self.direction, self.steps_per_circ))
+            self.geometry = DrawToolShape(LineString(arc(center, radius, startangle, stopangle,
+                                           self.direction, self.steps_per_circ)))
 
 
         elif self.mode == '132':
         elif self.mode == '132':
             p1 = array(self.points[0])
             p1 = array(self.points[0])
@@ -235,8 +251,8 @@ class FCArc(FCShapeTool):
             startangle = arctan2(p1[1] - center[1], p1[0] - center[0])
             startangle = arctan2(p1[1] - center[1], p1[0] - center[0])
             stopangle = arctan2(p3[1] - center[1], p3[0] - center[0])
             stopangle = arctan2(p3[1] - center[1], p3[0] - center[0])
 
 
-            self.geometry = LineString(arc(center, radius, startangle, stopangle,
-                                           direction, self.steps_per_circ))
+            self.geometry = DrawToolShape(LineString(arc(center, radius, startangle, stopangle,
+                                           direction, self.steps_per_circ)))
 
 
         else:  # self.mode == '12c'
         else:  # self.mode == '12c'
             p1 = array(self.points[0])
             p1 = array(self.points[0])
@@ -268,8 +284,8 @@ class FCArc(FCShapeTool):
             startangle = arctan2(p1[1] - center[1], p1[0] - center[0])
             startangle = arctan2(p1[1] - center[1], p1[0] - center[0])
             stopangle = arctan2(p2[1] - center[1], p2[0] - center[0])
             stopangle = arctan2(p2[1] - center[1], p2[0] - center[0])
 
 
-            self.geometry = LineString(arc(center, radius, startangle, stopangle,
-                                           self.direction, self.steps_per_circ))
+            self.geometry = DrawToolShape(LineString(arc(center, radius, startangle, stopangle,
+                                           self.direction, self.steps_per_circ)))
         self.complete = True
         self.complete = True
 
 
 
 
@@ -298,7 +314,7 @@ class FCRectangle(FCShapeTool):
         if len(self.points) == 1:
         if len(self.points) == 1:
             p1 = self.points[0]
             p1 = self.points[0]
             p2 = data
             p2 = data
-            return LinearRing([p1, (p2[0], p1[1]), p2, (p1[0], p2[1])])
+            return DrawToolUtilityShape(LinearRing([p1, (p2[0], p1[1]), p2, (p1[0], p2[1])]))
 
 
         return None
         return None
 
 
@@ -306,7 +322,7 @@ class FCRectangle(FCShapeTool):
         p1 = self.points[0]
         p1 = self.points[0]
         p2 = self.points[1]
         p2 = self.points[1]
         #self.geometry = LinearRing([p1, (p2[0], p1[1]), p2, (p1[0], p2[1])])
         #self.geometry = LinearRing([p1, (p2[0], p1[1]), p2, (p1[0], p2[1])])
-        self.geometry = Polygon([p1, (p2[0], p1[1]), p2, (p1[0], p2[1])])
+        self.geometry = DrawToolShape(Polygon([p1, (p2[0], p1[1]), p2, (p1[0], p2[1])]))
         self.complete = True
         self.complete = True
 
 
 
 
@@ -331,18 +347,18 @@ class FCPolygon(FCShapeTool):
         if len(self.points) == 1:
         if len(self.points) == 1:
             temp_points = [x for x in self.points]
             temp_points = [x for x in self.points]
             temp_points.append(data)
             temp_points.append(data)
-            return LineString(temp_points)
+            return DrawToolUtilityShape(LineString(temp_points))
 
 
         if len(self.points) > 1:
         if len(self.points) > 1:
             temp_points = [x for x in self.points]
             temp_points = [x for x in self.points]
             temp_points.append(data)
             temp_points.append(data)
-            return LinearRing(temp_points)
+            return DrawToolUtilityShape(LinearRing(temp_points))
 
 
         return None
         return None
 
 
     def make(self):
     def make(self):
         # self.geometry = LinearRing(self.points)
         # self.geometry = LinearRing(self.points)
-        self.geometry = Polygon(self.points)
+        self.geometry = DrawToolShape(Polygon(self.points))
         self.complete = True
         self.complete = True
 
 
 
 
@@ -352,14 +368,14 @@ class FCPath(FCPolygon):
     """
     """
 
 
     def make(self):
     def make(self):
-        self.geometry = LineString(self.points)
+        self.geometry = DrawToolShape(LineString(self.points))
         self.complete = True
         self.complete = True
 
 
     def utility_geometry(self, data=None):
     def utility_geometry(self, data=None):
         if len(self.points) > 1:
         if len(self.points) > 1:
             temp_points = [x for x in self.points]
             temp_points = [x for x in self.points]
             temp_points.append(data)
             temp_points.append(data)
-            return LineString(temp_points)
+            return DrawToolUtilityShape(LineString(temp_points))
 
 
         return None
         return None
 
 
@@ -368,6 +384,7 @@ class FCSelect(DrawTool):
     def __init__(self, draw_app):
     def __init__(self, draw_app):
         DrawTool.__init__(self, draw_app)
         DrawTool.__init__(self, draw_app)
         self.shape_buffer = self.draw_app.shape_buffer
         self.shape_buffer = self.draw_app.shape_buffer
+        self.selected = self.draw_app.selected
         self.start_msg = "Click on geometry to select"
         self.start_msg = "Click on geometry to select"
 
 
     def click(self, point):
     def click(self, point):
@@ -375,17 +392,21 @@ class FCSelect(DrawTool):
         closest_shape = None
         closest_shape = None
 
 
         for shape in self.shape_buffer:
         for shape in self.shape_buffer:
+
+            # Remove all if 'control' is not help
             if self.draw_app.key != 'control':
             if self.draw_app.key != 'control':
-                shape["selected"] = False
+                #shape["selected"] = False
+                self.draw_app.set_unselected(shape)
 
 
             # TODO: Do this with rtree?
             # TODO: Do this with rtree?
-            dist = Point(point).distance(shape["geometry"])
+            dist = Point(point).distance(cascaded_union(shape.geo))
             if dist < min_distance:
             if dist < min_distance:
                 closest_shape = shape
                 closest_shape = shape
                 min_distance = dist
                 min_distance = dist
 
 
         if closest_shape is not None:
         if closest_shape is not None:
-            closest_shape["selected"] = True
+            #closest_shape["selected"] = True
+            self.draw_app.set_selected(closest_shape)
             return "Shape selected."
             return "Shape selected."
 
 
         return "Nothing selected."
         return "Nothing selected."
@@ -403,6 +424,9 @@ class FCMove(FCShapeTool):
         self.origin = origin
         self.origin = origin
 
 
     def click(self, point):
     def click(self, point):
+        if len(self.draw_app.get_selected()) == 0:
+            return "Nothing to move."
+
         if self.origin is None:
         if self.origin is None:
             self.set_origin(point)
             self.set_origin(point)
             return "Click on final location."
             return "Click on final location."
@@ -415,11 +439,16 @@ class FCMove(FCShapeTool):
         # Create new geometry
         # Create new geometry
         dx = self.destination[0] - self.origin[0]
         dx = self.destination[0] - self.origin[0]
         dy = self.destination[1] - self.origin[1]
         dy = self.destination[1] - self.origin[1]
-        self.geometry = [affinity.translate(geom['geometry'], xoff=dx, yoff=dy) for geom in self.draw_app.get_selected()]
+        self.geometry = [DrawToolShape(affinity.translate(geom.geo, xoff=dx, yoff=dy))
+                                       for geom in self.draw_app.get_selected()]
 
 
         # Delete old
         # Delete old
-        for geo in self.draw_app.get_selected():
-            self.draw_app.shape_buffer.remove(geo)
+        self.draw_app.delete_selected()
+
+        # # Select the new
+        # for g in self.geometry:
+        #     # Note that g is not in the app's buffer yet!
+        #     self.draw_app.set_selected(g)
 
 
         self.complete = True
         self.complete = True
 
 
@@ -433,10 +462,14 @@ class FCMove(FCShapeTool):
         if self.origin is None:
         if self.origin is None:
             return None
             return None
 
 
+        if len(self.draw_app.get_selected()) == 0:
+            return None
+
         dx = data[0] - self.origin[0]
         dx = data[0] - self.origin[0]
         dy = data[1] - self.origin[1]
         dy = data[1] - self.origin[1]
 
 
-        return [affinity.translate(geom['geometry'], xoff=dx, yoff=dy) for geom in self.draw_app.get_selected()]
+        return DrawToolUtilityShape([affinity.translate(geom.geo, xoff=dx, yoff=dy)
+                                     for geom in self.draw_app.get_selected()])
 
 
 
 
 class FCCopy(FCMove):
 class FCCopy(FCMove):
@@ -444,7 +477,8 @@ class FCCopy(FCMove):
         # Create new geometry
         # Create new geometry
         dx = self.destination[0] - self.origin[0]
         dx = self.destination[0] - self.origin[0]
         dy = self.destination[1] - self.origin[1]
         dy = self.destination[1] - self.origin[1]
-        self.geometry = [affinity.translate(geom['geometry'], xoff=dx, yoff=dy) for geom in self.draw_app.get_selected()]
+        self.geometry = DrawToolShape([affinity.translate(geom['geometry'], xoff=dx, yoff=dy)
+                                       for geom in self.draw_app.get_selected()])
         self.complete = True
         self.complete = True
 
 
 
 
@@ -527,6 +561,7 @@ class FlatCAMDraw(QtCore.QObject):
         # Data
         # Data
         self.active_tool = None
         self.active_tool = None
         self.shape_buffer = []
         self.shape_buffer = []
+        self.selected = []
 
 
         self.move_timer = QtCore.QTimer()
         self.move_timer = QtCore.QTimer()
         self.move_timer.setSingleShot(True)
         self.move_timer.setSingleShot(True)
@@ -614,9 +649,10 @@ class FlatCAMDraw(QtCore.QObject):
 
 
         # Link shapes into editor.
         # Link shapes into editor.
         for shape in geometry:
         for shape in geometry:
-            self.shape_buffer.append({'geometry': shape,
-                                      'selected': False,
-                                      'utility': False})
+            # self.shape_buffer.append({'geometry': shape,
+            #                           # 'selected': False,
+            #                           'utility': False})
+            self.shape_buffer.append(DrawToolShape(geometry))
 
 
         self.replot()
         self.replot()
         self.drawing_toolbar.setDisabled(False)
         self.drawing_toolbar.setDisabled(False)
@@ -648,11 +684,11 @@ class FlatCAMDraw(QtCore.QObject):
 
 
     def on_canvas_click(self, event):
     def on_canvas_click(self, event):
         """
         """
-        event.x .y have canvas coordinates
-        event.xdaya .ydata have plot coordinates
+        event.x and .y have canvas coordinates
+        event.xdaya and .ydata have plot coordinates
 
 
-        :param event:
-        :return:
+        :param event: Event object dispatched by Matplotlib
+        :return: None
         """
         """
         if self.active_tool is not None:
         if self.active_tool is not None:
             # Dispatch event to active_tool
             # Dispatch event to active_tool
@@ -672,14 +708,14 @@ class FlatCAMDraw(QtCore.QObject):
 
 
     def on_canvas_move(self, event):
     def on_canvas_move(self, event):
         """
         """
-        event.x .y have canvas coordinates
-        event.xdaya .ydata have plot coordinates
+        event.x and .y have canvas coordinates
+        event.xdaya and .ydata have plot coordinates
 
 
-        :param event:
+        :param event: Event object dispatched by Matplotlib
         :return:
         :return:
         """
         """
         self.on_canvas_move_effective(event)
         self.on_canvas_move_effective(event)
-        return
+        return None
 
 
         # self.move_timer.stop()
         # self.move_timer.stop()
         #
         #
@@ -703,11 +739,11 @@ class FlatCAMDraw(QtCore.QObject):
         For details on animating on MPL see:
         For details on animating on MPL see:
         http://wiki.scipy.org/Cookbook/Matplotlib/Animations
         http://wiki.scipy.org/Cookbook/Matplotlib/Animations
 
 
-        event.x .y have canvas coordinates
-        event.xdaya .ydata have plot coordinates
+        event.x and .y have canvas coordinates
+        event.xdaya and .ydata have plot coordinates
 
 
-        :param event:
-        :return:
+        :param event: Event object dispatched by Matplotlib
+        :return: None
         """
         """
 
 
         try:
         try:
@@ -719,38 +755,33 @@ class FlatCAMDraw(QtCore.QObject):
         if self.active_tool is None:
         if self.active_tool is None:
             return
             return
 
 
+        ### Snap coordinates
         x, y = self.snap(x, y)
         x, y = self.snap(x, y)
 
 
         ### Utility geometry (animated)
         ### Utility geometry (animated)
         self.canvas.canvas.restore_region(self.canvas.background)
         self.canvas.canvas.restore_region(self.canvas.background)
         geo = self.active_tool.utility_geometry(data=(x, y))
         geo = self.active_tool.utility_geometry(data=(x, y))
 
 
-        if geo is not None and ((type(geo) == list and len(geo) > 0) or
-                                (type(geo) != list and not geo.is_empty)):
+        if isinstance(geo, DrawToolShape) and geo.geo is not None:
 
 
             # Remove any previous utility shape
             # Remove any previous utility shape
             for shape in self.shape_buffer:
             for shape in self.shape_buffer:
-                if shape['utility']:
+                if shape.utility:
                     self.shape_buffer.remove(shape)
                     self.shape_buffer.remove(shape)
 
 
             # Add the new utility shape
             # Add the new utility shape
-            self.shape_buffer.append({
-                'geometry': geo,
-                'selected': False,
-                'utility': True
-            })
+            self.shape_buffer.append(geo)
 
 
             # Efficient plotting for fast animation
             # Efficient plotting for fast animation
 
 
             #self.canvas.canvas.restore_region(self.canvas.background)
             #self.canvas.canvas.restore_region(self.canvas.background)
-            elements = self.plot_shape(geometry=geo, linespec="b--", animated=True)
+            elements = self.plot_shape(geometry=geo.geo, linespec="b--", animated=True)
             for el in elements:
             for el in elements:
                 self.axes.draw_artist(el)
                 self.axes.draw_artist(el)
             #self.canvas.canvas.blit(self.axes.bbox)
             #self.canvas.canvas.blit(self.axes.bbox)
 
 
             #self.replot()
             #self.replot()
 
 
-
         elements = self.axes.plot(x, y, 'bo', animated=True)
         elements = self.axes.plot(x, y, 'bo', animated=True)
         for el in elements:
         for el in elements:
                 self.axes.draw_artist(el)
                 self.axes.draw_artist(el)
@@ -780,7 +811,7 @@ class FlatCAMDraw(QtCore.QObject):
             # TODO: ...?
             # TODO: ...?
             self.on_tool_select("select")
             self.on_tool_select("select")
             self.app.info("Cancelled.")
             self.app.info("Cancelled.")
-            for_deletion = [shape for shape in self.shape_buffer if shape['utility']]
+            for_deletion = [shape for shape in self.shape_buffer if shape.utility]
             for shape in for_deletion:
             for shape in for_deletion:
                 self.shape_buffer.remove(shape)
                 self.shape_buffer.remove(shape)
 
 
@@ -821,18 +852,39 @@ class FlatCAMDraw(QtCore.QObject):
         self.key = None
         self.key = None
 
 
     def get_selected(self):
     def get_selected(self):
-        return [shape for shape in self.shape_buffer if shape["selected"]]
+        """
+        Returns list of shapes that are selected in the editor.
+
+        :return: List of shapes.
+        """
+        #return [shape for shape in self.shape_buffer if shape["selected"]]
+        return self.selected
 
 
     def delete_selected(self):
     def delete_selected(self):
-        for shape in self.get_selected():
+        # for shape in self.get_selected():
+        #     self.shape_buffer.remove(shape)
+        #     self.app.info("Shape deleted.")
+        for shape in self.selected:
             self.shape_buffer.remove(shape)
             self.shape_buffer.remove(shape)
-            self.app.info("Shape deleted.")
+
+        self.selected = []
 
 
     def plot_shape(self, geometry=None, linespec='b-', linewidth=1, animated=False):
     def plot_shape(self, geometry=None, linespec='b-', linewidth=1, animated=False):
+        """
+        Plots a geometric object or list of objects without rendeting. Plotted objects
+        are returned as a list. This allows for efficient/animated rendering.
+
+        :param geometry: Geometry to be plotted (Any Shapely.geom kind or list of such)
+        :param linespec: Matplotlib linespec string.
+        :param linewidth: Width of lines in # of pixels.
+        :param animated: If geometry is to be animated. (See MPL plot())
+        :return: List of plotted elements.
+        """
         plot_elements = []
         plot_elements = []
 
 
         if geometry is None:
         if geometry is None:
             geometry = self.active_tool.geometry
             geometry = self.active_tool.geometry
+
         try:
         try:
             _ = iter(geometry)
             _ = iter(geometry)
             iterable_geometry = geometry
             iterable_geometry = geometry
@@ -881,22 +933,29 @@ class FlatCAMDraw(QtCore.QObject):
         self.app.log.debug("plot_all()")
         self.app.log.debug("plot_all()")
         self.axes.cla()
         self.axes.cla()
         for shape in self.shape_buffer:
         for shape in self.shape_buffer:
-            if shape['geometry'] is None:  # TODO: This shouldn't have happened
+            if shape.geo is None:  # TODO: This shouldn't have happened
                 continue
                 continue
 
 
-            if shape['utility']:
-                self.plot_shape(geometry=shape['geometry'], linespec='k--', linewidth=1)
+            if shape.utility:
+                self.plot_shape(geometry=shape.geo, linespec='k--', linewidth=1)
                 continue
                 continue
 
 
-            if shape['selected']:
-                self.plot_shape(geometry=shape['geometry'], linespec='k-', linewidth=2)
+            if shape in self.selected:
+                self.plot_shape(geometry=shape.geo, linespec='k-', linewidth=2)
                 continue
                 continue
 
 
-            self.plot_shape(geometry=shape['geometry'])
+            self.plot_shape(geometry=shape.geo)
 
 
         self.canvas.auto_adjust_axes()
         self.canvas.auto_adjust_axes()
 
 
     def add2index(self, id, geo):
     def add2index(self, id, geo):
+        """
+
+
+        :param id: Index of data in list being indexed.
+        :param geo: Some Shapely.geom kind
+        :return: None
+        """
         try:
         try:
             for pt in geo.coords:
             for pt in geo.coords:
                 self.rtree_index.add(id, pt)
                 self.rtree_index.add(id, pt)
@@ -914,31 +973,60 @@ class FlatCAMDraw(QtCore.QObject):
         #self.plot_shape()
         #self.plot_shape()
         #self.canvas.auto_adjust_axes()
         #self.canvas.auto_adjust_axes()
 
 
-        try:
-            for geo in self.active_tool.geometry:
-                self.shape_buffer.append({'geometry': geo,
-                                          'selected': False,
-                                          'utility': False})
-                self.add2index(len(self.shape_buffer)-1, geo)
-        except TypeError:
-            self.shape_buffer.append({'geometry': self.active_tool.geometry,
-                                      'selected': False,
-                                      'utility': False})
-            self.add2index(len(self.shape_buffer)-1, self.active_tool.geometry)
+        self.add_shape(self.active_tool.geometry)
 
 
         # Remove any utility shapes
         # Remove any utility shapes
         for shape in self.shape_buffer:
         for shape in self.shape_buffer:
-            if shape['utility']:
+            if shape.utility:
                 self.shape_buffer.remove(shape)
                 self.shape_buffer.remove(shape)
 
 
         self.replot()
         self.replot()
         self.active_tool = type(self.active_tool)(self)
         self.active_tool = type(self.active_tool)(self)
 
 
+    def add_shape(self, shape):
+        """
+        Adds a shape to the shape buffer and the rtree index.
+
+        :param shape: Shape to be added.
+        :type shape: DrawToolShape
+        :return: None
+        """
+        print "add_shape()"
+
+        # List ?
+        if isinstance(shape, list):
+            for subshape in shape:
+                self.add_shape(subshape)
+            return
+
+        assert isinstance(shape, DrawToolShape)
+        assert shape.geo is not None
+        assert (isinstance(shape.geo, list) and len(shape.geo) > 0) or not isinstance(shape.geo, list)
+        try:
+            for geo in shape.geo:
+                self.add2index(len(self.shape_buffer), geo)
+            self.shape_buffer.append(shape)
+        except TypeError:
+            self.shape_buffer.append(shape)
+            self.add2index(len(self.shape_buffer) - 1, shape.geo)
+
     def replot(self):
     def replot(self):
         #self.canvas.clear()
         #self.canvas.clear()
         self.axes = self.canvas.new_axes("draw")
         self.axes = self.canvas.new_axes("draw")
         self.plot_all()
         self.plot_all()
 
 
+    def set_selected(self, shape):
+
+        # Remove and add to the end.
+        if shape in self.selected:
+            self.selected.remove(shape)
+
+        self.selected.append(shape)
+
+    def set_unselected(self, shape):
+        if shape in self.selected:
+            self.selected.remove(shape)
+
     def snap(self, x, y):
     def snap(self, x, y):
         """
         """
         Adjusts coordinates to snap settings.
         Adjusts coordinates to snap settings.
@@ -967,12 +1055,12 @@ class FlatCAMDraw(QtCore.QObject):
         ### Grid snap
         ### Grid snap
         if self.options["grid_snap"]:
         if self.options["grid_snap"]:
             if self.options["snap-x"] != 0:
             if self.options["snap-x"] != 0:
-                snap_x_ = round(x/self.options["snap-x"])*self.options['snap-x']
+                snap_x_ = round(x / self.options["snap-x"]) * self.options['snap-x']
             else:
             else:
                 snap_x_ = x
                 snap_x_ = x
 
 
             if self.options["snap-y"] != 0:
             if self.options["snap-y"] != 0:
-                snap_y_ = round(y/self.options["snap-y"])*self.options['snap-y']
+                snap_y_ = round(y / self.options["snap-y"]) * self.options['snap-y']
             else:
             else:
                 snap_y_ = y
                 snap_y_ = y
             nearest_grid_distance = distance((x, y), (snap_x_, snap_y_))
             nearest_grid_distance = distance((x, y), (snap_x_, snap_y_))
@@ -991,7 +1079,7 @@ class FlatCAMDraw(QtCore.QObject):
         """
         """
         fcgeometry.solid_geometry = []
         fcgeometry.solid_geometry = []
         for shape in self.shape_buffer:
         for shape in self.shape_buffer:
-            fcgeometry.solid_geometry.append(shape['geometry'])
+            fcgeometry.solid_geometry.append(shape.geo)
 
 
     def union(self):
     def union(self):
         """
         """
@@ -1000,27 +1088,24 @@ class FlatCAMDraw(QtCore.QObject):
 
 
         :return: None.
         :return: None.
         """
         """
-        targets = [shape for shape in self.shape_buffer if shape['selected']]
+        #targets = [shape for shape in self.selected]
 
 
-        results = cascaded_union([t['geometry'] for t in targets])
+        results = cascaded_union([t.geo for t in self.get_selected()])
 
 
-        for shape in targets:
+        # Delete originals.
+        for shape in self.get_selected():
             self.shape_buffer.remove(shape)
             self.shape_buffer.remove(shape)
 
 
-        try:
-            for geo in results:
+        # Selected geometry is now gone!
+        self.selected = []
 
 
-                self.shape_buffer.append({
-                    'geometry': geo,
-                    'selected': True,
-                    'utility': False
-                })
-        except TypeError:
-            self.shape_buffer.append({
-                'geometry': results,
-                'selected': True,
-                'utility': False
-            })
+        # try:
+        #     for geo in results:
+        #         self.shape_buffer.append(DrawToolShape(geo))
+        # except TypeError:
+        #     self.shape_buffer.append(DrawToolShape(geo))
+
+        self.add_shape(DrawToolShape(results))
 
 
         self.replot()
         self.replot()
 
 

+ 45 - 0
doc/source/planning.rst

@@ -0,0 +1,45 @@
+Development Planning
+====================
+
+Drawing
+-------
+
+* [DONE] Arcs
+* Subtract Shapes
+  * Selected objects must be kept onlist to preserve order.
+* Polygon to outline
+* Force perpendicular
+
+
+Algorithms
+----------
+
+* Reverse path if end is nearer.
+* Seed paint: Specify seed.
+
+
+Features
+--------
+
+* Z profile
+* UNDO
+
+
+G-Code
+------
+
+* More output options: Formatting.
+* Don't lift the tool if unnecessary.
+
+
+Excellon
+--------
+
+* Parse tool definitions in body
+
+
+Bugs
+----
+
+* Unit conversion on opening.
+* `cascaded_union([])` bug requires more testing.

+ 4 - 0
manual/editor.rst

@@ -59,6 +59,7 @@ Creating Shapes
 The shape creation tools in the editor are:
 The shape creation tools in the editor are:
 
 
 * Circle
 * Circle
+* Arc
 * Rectangle
 * Rectangle
 * Polygon
 * Polygon
 * Path
 * Path
@@ -72,6 +73,9 @@ on the status bar.
 Shapes that do not require a fixed number of clicks to complete, like
 Shapes that do not require a fixed number of clicks to complete, like
 polygons and paths, are complete by hitting the ``Space`` key.
 polygons and paths, are complete by hitting the ``Space`` key.
 
 
+Certain shape tools can have different options or modes. By hitting
+`o` and/or `p` the tool will cycle through its options and/or modes.
+
 .. seealso::
 .. seealso::
 
 
    The FlatCAM Shell commands :ref:`add_circle`, :ref:`add_poly` and :ref:`add_rect`,
    The FlatCAM Shell commands :ref:`add_circle`, :ref:`add_poly` and :ref:`add_rect`,