فهرست منبع

Merged jpcgt/flatcam into master

Kamil Sopko 11 سال پیش
والد
کامیت
dd7177bf3a
9فایلهای تغییر یافته به همراه407 افزوده شده و 263 حذف شده
  1. 1 0
      .gitignore
  2. 206 141
      FlatCAMDraw.py
  3. 1 55
      README.md
  4. 150 64
      camlib.py
  5. 12 3
      doc/source/planning.rst
  6. BIN
      share/deleteshape16.png
  7. BIN
      share/deleteshape24.png
  8. BIN
      share/deleteshape32.png
  9. 37 0
      tests/test_fcrts.py

+ 1 - 0
.gitignore

@@ -0,0 +1 @@
+*.pyc

+ 206 - 141
FlatCAMDraw.py

@@ -21,12 +21,52 @@ from rtree import index as rtindex
 
 class DrawToolShape(object):
 
+    @staticmethod
+    def get_pts(o):
+        """
+        Returns a list of all points in the object, where
+        the object can be a Polygon, Not a polygon, or a list
+        of such. Search is done recursively.
+
+        :param: geometric object
+        :return: List of points
+        :rtype: list
+        """
+        pts = []
+
+        ## Iterable: descend into each item.
+        try:
+            for subo in o:
+                pts += DrawToolShape.get_pts(subo)
+
+        ## Non-iterable
+        except TypeError:
+
+            ## DrawToolShape: descend into .geo.
+            if isinstance(o, DrawToolShape):
+                pts += DrawToolShape.get_pts(o.geo)
+
+            ## Descend into .exerior and .interiors
+            elif type(o) == Polygon:
+                pts += DrawToolShape.get_pts(o.exterior)
+                for i in o.interiors:
+                    pts += DrawToolShape.get_pts(i)
+
+            ## Has .coords: list them.
+            else:
+                pts += list(o.coords)
+
+        return pts
+
     def __init__(self, geo=[]):
 
         # Shapely type or list of such
         self.geo = geo
         self.utility = False
 
+    def get_all_points(self):
+        return DrawToolShape.get_pts(self)
+
 
 class DrawToolUtilityShape(DrawToolShape):
 
@@ -383,39 +423,27 @@ class FCPath(FCPolygon):
 class FCSelect(DrawTool):
     def __init__(self, draw_app):
         DrawTool.__init__(self, draw_app)
-        self.shape_buffer = self.draw_app.shape_buffer
+        self.storage = self.draw_app.storage
+        #self.shape_buffer = self.draw_app.shape_buffer
         self.selected = self.draw_app.selected
         self.start_msg = "Click on geometry to select"
 
     def click(self, point):
-        min_distance = Inf
-        closest_shape = None
-
-        for shape in self.shape_buffer:
+        _, closest_shape = self.storage.nearest(point)
 
-            # Remove all if 'control' is not help
-            if self.draw_app.key != 'control':
-                #shape["selected"] = False
-                self.draw_app.set_unselected(shape)
+        if self.draw_app.key != 'control':
+            self.draw_app.selected = []
 
-            # TODO: Do this with rtree?
-            dist = Point(point).distance(cascaded_union(shape.geo))
-            if dist < min_distance:
-                closest_shape = shape
-                min_distance = dist
+        self.draw_app.set_selected(closest_shape)
+        self.draw_app.app.log.debug("Selected shape containing: " + str(closest_shape.geo))
 
-        if closest_shape is not None:
-            #closest_shape["selected"] = True
-            self.draw_app.set_selected(closest_shape)
-            return "Shape selected."
-
-        return "Nothing selected."
+        return ""
 
 
 class FCMove(FCShapeTool):
     def __init__(self, draw_app):
         FCShapeTool.__init__(self, draw_app)
-        self.shape_buffer = self.draw_app.shape_buffer
+        #self.shape_buffer = self.draw_app.shape_buffer
         self.origin = None
         self.destination = None
         self.start_msg = "Click on reference point."
@@ -498,7 +526,7 @@ class FlatCAMDraw(QtCore.QObject):
         self.drawing_toolbar = QtGui.QToolBar()
         self.drawing_toolbar.setDisabled(disabled)
         self.app.ui.addToolBar(self.drawing_toolbar)
-        self.select_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/pointer32.png'), 'Select')
+        self.select_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/pointer32.png'), "Select 'Esc'")
         self.add_circle_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/circle32.png'), 'Add Circle')
         self.add_arc_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/arc32.png'), 'Add Arc')
         self.add_rectangle_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/rectangle32.png'), 'Add Rectangle')
@@ -507,8 +535,9 @@ class FlatCAMDraw(QtCore.QObject):
         self.union_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/union32.png'), 'Polygon Union')
         self.subtract_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/subtract32.png'), 'Polygon Subtraction')
         self.cutpath_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/cutpath32.png'), 'Cut Path')
-        self.move_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/move32.png'), 'Move Objects')
-        self.copy_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/copy32.png'), 'Copy Objects')
+        self.move_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/move32.png'), "Move Objects 'm'")
+        self.copy_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/copy32.png'), "Copy Objects 'c'")
+        self.delete_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/deleteshape32.png'), "Delete Shape '-'")
 
         ### Snap Toolbar ###
         self.snap_toolbar = QtGui.QToolBar()
@@ -541,6 +570,7 @@ class FlatCAMDraw(QtCore.QObject):
         self.union_btn.triggered.connect(self.union)
         self.subtract_btn.triggered.connect(self.subtract)
         self.cutpath_btn.triggered.connect(self.cutpath)
+        self.delete_btn.triggered.connect(self.on_delete_btn)
 
         ## Toolbar events and properties
         self.tools = {
@@ -565,12 +595,8 @@ class FlatCAMDraw(QtCore.QObject):
         ### Data
         self.active_tool = None
 
-        ## List of shapes, None for removed ones. List
-        ## never decreases size.
-        self.main_index = []
-
-        ## List of shapes.
-        self.shape_buffer = []
+        self.storage = FlatCAMDraw.make_storage()
+        self.utility = []
 
         ## List of selected shapes.
         self.selected = []
@@ -580,9 +606,9 @@ class FlatCAMDraw(QtCore.QObject):
 
         self.key = None  # Currently pressed key
 
-        def make_callback(tool):
+        def make_callback(thetool):
             def f():
-                self.on_tool_select(tool)
+                self.on_tool_select(thetool)
             return f
 
         for tool in self.tools:
@@ -624,11 +650,42 @@ class FlatCAMDraw(QtCore.QObject):
     def activate(self):
         pass
 
+    def add_shape(self, shape):
+        """
+        Adds a shape to the shape storage.
+
+        :param shape: Shape to be added.
+        :type shape: DrawToolShape
+        :return: None
+        """
+
+        # List of DrawToolShape?
+        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)
+
+        if isinstance(shape, DrawToolUtilityShape):
+            self.utility.append(shape)
+        else:
+            self.storage.insert(shape)
+
     def deactivate(self):
         self.clear()
         self.drawing_toolbar.setDisabled(True)
         self.snap_toolbar.setDisabled(True)  # TODO: Combine and move into tool
 
+    def delete_utility_geometry(self):
+        #for_deletion = [shape for shape in self.shape_buffer if shape.utility]
+        #for_deletion = [shape for shape in self.storage.get_objects() if shape.utility]
+        for_deletion = [shape for shape in self.utility]
+        for shape in for_deletion:
+            self.delete_shape(shape)
+
     def cutpath(self):
         selected = self.get_selected()
         tools = selected[1:]
@@ -649,11 +706,12 @@ class FlatCAMDraw(QtCore.QObject):
 
     def toolbar_tool_toggle(self, key):
         self.options[key] = self.sender().isChecked()
-        print "grid_snap", self.options["grid_snap"]
 
     def clear(self):
         self.active_tool = None
-        self.shape_buffer = []
+        #self.shape_buffer = []
+        self.selected = []
+        self.storage = FlatCAMDraw.make_storage()
         self.replot()
 
     def edit_fcgeometry(self, fcgeometry):
@@ -664,25 +722,14 @@ class FlatCAMDraw(QtCore.QObject):
         :param fcgeometry: FlatCAMGeometry
         :return: None
         """
+        assert isinstance(fcgeometry, Geometry)
 
-        if fcgeometry.solid_geometry is None:
-            geometry = []
-        else:
-            try:
-                _ = iter(fcgeometry.solid_geometry)
-                geometry = fcgeometry.solid_geometry
-            except TypeError:
-                geometry = [fcgeometry.solid_geometry]
-
-        # Delete contents of editor.
-        self.shape_buffer = []
+        self.clear()
 
         # Link shapes into editor.
-        for shape in geometry:
-            # self.shape_buffer.append({'geometry': shape,
-            #                           # 'selected': False,
-            #                           'utility': False})
-            self.shape_buffer.append(DrawToolShape(geometry))
+        for shape in fcgeometry.flatten():
+            if shape is not None:  # TODO: Make flatten never create a None
+                self.add_shape(DrawToolShape(shape))
 
         self.replot()
         self.drawing_toolbar.setDisabled(False)
@@ -795,9 +842,7 @@ class FlatCAMDraw(QtCore.QObject):
         if isinstance(geo, DrawToolShape) and geo.geo is not None:
 
             # Remove any previous utility shape
-            for shape in self.shape_buffer:
-                if shape.utility:
-                    self.shape_buffer.remove(shape)
+            self.delete_utility_geometry()
 
             # Add the new utility shape
             self.add_shape(geo)
@@ -805,7 +850,10 @@ class FlatCAMDraw(QtCore.QObject):
             # Efficient plotting for fast animation
 
             #self.canvas.canvas.restore_region(self.canvas.background)
-            elements = self.plot_shape(geometry=geo.geo, linespec="b--", animated=True)
+            elements = self.plot_shape(geometry=geo.geo,
+                                       linespec="b--",
+                                       linewidth=1,
+                                       animated=True)
             for el in elements:
                 self.axes.draw_artist(el)
             #self.canvas.canvas.blit(self.axes.bbox)
@@ -841,9 +889,8 @@ class FlatCAMDraw(QtCore.QObject):
             # TODO: ...?
             self.on_tool_select("select")
             self.app.info("Cancelled.")
-            for_deletion = [shape for shape in self.shape_buffer if shape.utility]
-            for shape in for_deletion:
-                self.shape_buffer.remove(shape)
+
+            self.delete_utility_geometry()
 
             self.replot()
             self.select_btn.setChecked(True)
@@ -881,6 +928,10 @@ class FlatCAMDraw(QtCore.QObject):
     def on_canvas_key_release(self, event):
         self.key = None
 
+    def on_delete_btn(self):
+        self.delete_selected()
+        self.replot()
+
     def get_selected(self):
         """
         Returns list of shapes that are selected in the editor.
@@ -917,93 +968,94 @@ class FlatCAMDraw(QtCore.QObject):
         if geometry is None:
             geometry = self.active_tool.geometry
 
-        try:
-            _ = iter(geometry)
-            iterable_geometry = geometry
-        except TypeError:
-            iterable_geometry = [geometry]
+        # try:
+        #     _ = iter(geometry)
+        #     iterable_geometry = geometry
+        # except TypeError:
+        #     iterable_geometry = [geometry]
 
-        for geo in iterable_geometry:
+        ## Iterable: Descend into each element.
+        try:
+            for geo in geometry:
+                plot_elements += self.plot_shape(geometry=geo,
+                                                 linespec=linespec,
+                                                 linewidth=linewidth,
+                                                 animated=animated)
 
-            if type(geo) == Polygon:
-                x, y = geo.exterior.coords.xy
-                element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
-                plot_elements.append(element)
-                for ints in geo.interiors:
-                    x, y = ints.coords.xy
-                    element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
-                    plot_elements.append(element)
-                continue
+        ## Non-iterable
+        except TypeError:
 
-            if type(geo) == LineString or type(geo) == LinearRing:
-                x, y = geo.coords.xy
+            ## DrawToolShape
+            if isinstance(geometry, DrawToolShape):
+                plot_elements += self.plot_shape(geometry=geometry.geo,
+                                                 linespec=linespec,
+                                                 linewidth=linewidth,
+                                                 animated=animated)
+
+            ## Polygon: Dscend into exterior and each interior.
+            if type(geometry) == Polygon:
+                plot_elements += self.plot_shape(geometry=geometry.exterior,
+                                                 linespec=linespec,
+                                                 linewidth=linewidth,
+                                                 animated=animated)
+                plot_elements += self.plot_shape(geometry=geometry.interiors,
+                                                 linespec=linespec,
+                                                 linewidth=linewidth,
+                                                 animated=animated)
+
+                # x, y = geo.exterior.coords.xy
+                # element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
+                # plot_elements.append(element)
+                # for ints in geo.interiors:
+                #     x, y = ints.coords.xy
+                #     element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
+                #     plot_elements.append(element)
+                # continue
+
+            if type(geometry) == LineString or type(geometry) == LinearRing:
+                x, y = geometry.coords.xy
                 element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
                 plot_elements.append(element)
-                continue
-
-            if type(geo) == MultiPolygon:
-                for poly in geo:
-                    x, y = poly.exterior.coords.xy
-                    element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
-                    plot_elements.append(element)
-                    for ints in poly.interiors:
-                        x, y = ints.coords.xy
-                        element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
-                        plot_elements.append(element)
-                continue
-
-            if type(geo) == Point:
-                x, y = geo.coords.xy
+                # continue
+
+            # if type(geo) == MultiPolygon:
+            #     for poly in geo:
+            #         x, y = poly.exterior.coords.xy
+            #         element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
+            #         plot_elements.append(element)
+            #         for ints in poly.interiors:
+            #             x, y = ints.coords.xy
+            #             element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
+            #             plot_elements.append(element)
+            #     continue
+
+            if type(geometry) == Point:
+                x, y = geometry.coords.xy
                 element, = self.axes.plot(x, y, 'bo', linewidth=linewidth, animated=animated)
                 plot_elements.append(element)
-                continue
+                # continue
 
         return plot_elements
         # self.canvas.auto_adjust_axes()
 
-    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
-        """
-
-        # List of DrawToolShape?
-        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)
-
-        self.shape_buffer.append(shape)
-
-        # Do not add utility shapes to the index.
-        if not isinstance(shape, DrawToolUtilityShape):
-            self.main_index.append(shape)
-            self.add2index(len(self.main_index) - 1, shape)
-
     def plot_all(self):
         self.app.log.debug("plot_all()")
         self.axes.cla()
-        for shape in self.shape_buffer:
+        #for shape in self.shape_buffer:
+        for shape in self.storage.get_objects():
             if shape.geo is None:  # TODO: This shouldn't have happened
                 continue
 
-            if shape.utility:
-                self.plot_shape(geometry=shape.geo, linespec='k--', linewidth=1)
-                continue
-
             if shape in self.selected:
                 self.plot_shape(geometry=shape.geo, linespec='k-', linewidth=2)
                 continue
 
             self.plot_shape(geometry=shape.geo)
 
+        for shape in self.utility:
+            self.plot_shape(geometry=shape.geo, linespec='k--', linewidth=1)
+            continue
+
         self.canvas.auto_adjust_axes()
 
     def add2index(self, id, geo):
@@ -1076,26 +1128,29 @@ class FlatCAMDraw(QtCore.QObject):
         self.add_shape(self.active_tool.geometry)
 
         # Remove any utility shapes
-        for shape in self.shape_buffer:
-            if shape.utility:
-                self.shape_buffer.remove(shape)
+        self.delete_utility_geometry()
 
         self.replot()
         self.active_tool = type(self.active_tool)(self)
 
     def delete_shape(self, shape):
-        try:
-            # Remove from index list
-            shp_idx = self.main_index.index(shape)
-            self.main_index[shp_idx] = None
-
-            # Remove from rtree index
-            self.remove_from_index(shp_idx, shape)
-        except ValueError:
-            pass
+        # try:
+        #     # Remove from index list
+        #     shp_idx = self.main_index.index(shape)
+        #     self.main_index[shp_idx] = None
+        #
+        #     # Remove from rtree index
+        #     self.remove_from_index(shp_idx, shape)
+        # except ValueError:
+        #     pass
+        #
+        # if shape in self.shape_buffer:
+        #     self.shape_buffer.remove(shape)
+        if shape in self.utility:
+            self.utility.remove(shape)
+            return
 
-        if shape in self.shape_buffer:
-            self.shape_buffer.remove(shape)
+        self.storage.remove(shape)
 
         if shape in self.selected:
             self.selected.remove(shape)
@@ -1105,6 +1160,15 @@ class FlatCAMDraw(QtCore.QObject):
         self.axes = self.canvas.new_axes("draw")
         self.plot_all()
 
+    @staticmethod
+    def make_storage():
+
+        ## Shape storage.
+        storage = FlatCAMRTreeStorage()
+        storage.get_points = DrawToolShape.get_pts
+
+        return storage
+
     def set_selected(self, shape):
 
         # Remove and add to the end.
@@ -1134,14 +1198,13 @@ class FlatCAMDraw(QtCore.QObject):
         ### in the index.
         if self.options["corner_snap"]:
             try:
-                bbox = self.rtree_index.nearest((x, y), objects=True).next().bbox
-                nearest_pt = (bbox[0], bbox[1])
+                nearest_pt, shape = self.storage.nearest((x, y))
 
                 nearest_pt_distance = distance((x, y), nearest_pt)
                 if nearest_pt_distance <= self.options["snap_max"]:
                     snap_distance = nearest_pt_distance
                     snap_x, snap_y = nearest_pt
-            except StopIteration:
+            except (StopIteration, AssertionError):
                 pass
 
         ### Grid snap
@@ -1170,7 +1233,8 @@ class FlatCAMDraw(QtCore.QObject):
         :return: None
         """
         fcgeometry.solid_geometry = []
-        for shape in self.shape_buffer:
+        #for shape in self.shape_buffer:
+        for shape in self.storage.get_objects():
             fcgeometry.solid_geometry.append(shape.geo)
 
     def union(self):
@@ -1184,8 +1248,9 @@ class FlatCAMDraw(QtCore.QObject):
         results = cascaded_union([t.geo for t in self.get_selected()])
 
         # Delete originals.
-        for shape in self.get_selected():
-            self.shape_buffer.remove(shape)
+        for_deletion = [s for s in self.get_selected()]
+        for shape in for_deletion:
+            self.delete_shape(shape)
 
         # Selected geometry is now gone!
         self.selected = []

+ 1 - 55
README.md

@@ -5,58 +5,4 @@ FlatCAM: 2D Post-processing for Manufacturing
 
 FlatCAM is a program for preparing CNC jobs for making PCBs on a CNC router.
 Among other things, it can take a Gerber file generated by your favorite PCB
-CAD program, and create G-Code for Isolation routing. But there's more.
-
-
-
-
-
-This  fork is  mainly for improving shell  commands.
-
-added so far:
-
-* cutout
-* mirror
-* cncdrilljob
-
-
-todo:
-
-* commandline  witch  reads  whole shell sequence from given file
-
-
-example of  shell flow:
-
-```
-#!flatcam shell
-
-
-new 
-open_gerber /home/sopak/kicad/ThermalShield/Gerber/ThermalPicoShield2-Margin.gbr  -outname Margin
-open_gerber /home/sopak/kicad/ThermalShield/Gerber/ThermalPicoShield2-B_Cu.gbr  -outname BottomCu
-open_excellon /home/sopak/kicad/ThermalShield/Gerber/ThermalPicoShield2.drl -outname Drills
-
-mirror BottomCu -box Margin -axis X
-
-mirror Drills -box Margin -axis X
-
-cutout Margin -dia 3 -margin 0 -gapsize 0.6 -gaps lr
-
-isolate BottomCu -dia 0.4 -overlap 1
-
-drillcncjob Drills -tools 1 -drillz -2 -travelz 2 -feedrate 5 -outname Drills_cncjob_0.8
-
-drillcncjob Drills -tools 2 -drillz -2 -travelz 2 -feedrate 5 -outname Drills_cncjob_3.0
-
-cncjob BottomCu_iso -tooldia 0.4
-
-cncjob Margin_cutout -tooldia 3
-
-write_gcode BottomCu_iso_cnc /home/sopak/kicad/ThermalShield/Gerber/ThermalPicoShield2-B_Cu.gbr_iso_cnc.ngc
-
-write_gcode Margin_cutout_cnc /home/sopak/kicad/ThermalShield/Gerber/ThermalPicoShield2-Margin.gbr_cutout_cnc.ngc
-
-write_gcode Drills_cncjob_3.0 /home/sopak/kicad/ThermalShield/Gerber/ThermalPicoShield2.drl_Drills_cncjob_3.0.ngc
-
-write_gcode Drills_cncjob_0.8 /home/sopak/kicad/ThermalShield/Gerber/ThermalPicoShield2.drl_Drills_cncjob_0.8.ngc
-```
+CAD program, and create G-Code for Isolation routing.

+ 150 - 64
camlib.py

@@ -138,16 +138,7 @@ class Geometry(object):
         else:
             return self.solid_geometry.bounds
 
-    def flatten_to_paths(self, geometry=None, reset=True):
-        """
-        Creates a list of non-iterable linear geometry elements and
-        indexes them in rtree.
-
-        :param geometry: Iterable geometry
-        :param reset: Wether to clear (True) or append (False) to self.flat_geometry
-        :return: self.flat_geometry, self.flat_geometry_rtree
-        """
-
+    def flatten(self, geometry=None, reset=True, pathonly=False):
         if geometry is None:
             geometry = self.solid_geometry
 
@@ -157,30 +148,88 @@ class Geometry(object):
         ## If iterable, expand recursively.
         try:
             for geo in geometry:
-                self.flatten_to_paths(geometry=geo, reset=False)
+                self.flatten(geometry=geo,
+                             reset=False,
+                             pathonly=pathonly)
 
         ## Not iterable, do the actual indexing and add.
         except TypeError:
-            if type(geometry) == Polygon:
-                g = geometry.exterior
-                self.flat_geometry.append(g)
-
-                ## Add first and last points of the path to the index.
-                self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[0])
-                self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[-1])
-
-                for interior in geometry.interiors:
-                    g = interior
-                    self.flat_geometry.append(g)
-                    self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[0])
-                    self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[-1])
+            if pathonly and type(geometry) == Polygon:
+                self.flat_geometry.append(geometry.exterior)
+                self.flatten(geometry=geometry.interiors,
+                             reset=False,
+                             pathonly=True)
             else:
-                g = geometry
-                self.flat_geometry.append(g)
-                self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[0])
-                self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[-1])
+                self.flat_geometry.append(geometry)
+            # if type(geometry) == Polygon:
+            #     self.flat_geometry.append(geometry)
+
+        return self.flat_geometry
+
+    def make2Dindex(self):
 
-        return self.flat_geometry, self.flat_geometry_rtree
+        self.flatten()
+
+        def get_pts(o):
+            pts = []
+            if type(o) == Polygon:
+                g = o.exterior
+                pts += list(g.coords)
+                for i in o.interiors:
+                    pts += list(i.coords)
+            else:
+                pts += list(o.coords)
+            return pts
+
+        idx = FlatCAMRTreeStorage()
+        idx.get_points = get_pts
+        for shape in self.flat_geometry:
+            idx.insert(shape)
+        return idx
+
+    # def flatten_to_paths(self, geometry=None, reset=True):
+    #     """
+    #     Creates a list of non-iterable linear geometry elements and
+    #     indexes them in rtree.
+    #
+    #     :param geometry: Iterable geometry
+    #     :param reset: Wether to clear (True) or append (False) to self.flat_geometry
+    #     :return: self.flat_geometry, self.flat_geometry_rtree
+    #     """
+    #
+    #     if geometry is None:
+    #         geometry = self.solid_geometry
+    #
+    #     if reset:
+    #         self.flat_geometry = []
+    #
+    #     ## If iterable, expand recursively.
+    #     try:
+    #         for geo in geometry:
+    #             self.flatten_to_paths(geometry=geo, reset=False)
+    #
+    #     ## Not iterable, do the actual indexing and add.
+    #     except TypeError:
+    #         if type(geometry) == Polygon:
+    #             g = geometry.exterior
+    #             self.flat_geometry.append(g)
+    #
+    #             ## Add first and last points of the path to the index.
+    #             self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[0])
+    #             self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[-1])
+    #
+    #             for interior in geometry.interiors:
+    #                 g = interior
+    #                 self.flat_geometry.append(g)
+    #                 self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[0])
+    #                 self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[-1])
+    #         else:
+    #             g = geometry
+    #             self.flat_geometry.append(g)
+    #             self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[0])
+    #             self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[-1])
+    #
+    #     return self.flat_geometry, self.flat_geometry_rtree
 
     def isolation_geometry(self, offset):
         """
@@ -2282,10 +2331,21 @@ class CNCjob(Geometry):
         """
         assert isinstance(geometry, Geometry)
 
-        ## Flatten the geometry and get rtree index
-        flat_geometry, rti = geometry.flatten_to_paths()
+        ## Flatten the geometry
+        flat_geometry = geometry.flatten(pathonly=True)
         log.debug("%d paths" % len(flat_geometry))
 
+        ## Index first and last points in paths
+        def get_pts(o):
+            return [o.coords[0], o.coords[-1]]
+
+        storage = FlatCAMRTreeStorage()
+        storage.get_points = get_pts
+
+        for shape in flat_geometry:
+            if shape is not None:  # TODO: This shouldn't have happened.
+                storage.insert(shape)
+
         if tooldia is not None:
             self.tooldia = tooldia
 
@@ -2306,37 +2366,39 @@ class CNCjob(Geometry):
         ## Iterate over geometry paths getting the nearest each time.
         path_count = 0
         current_pt = (0, 0)
-        hits = list(rti.nearest(current_pt, 1))
-        while len(hits) > 0:
-            path_count += 1
-            print "Current: ", "(%.3f, %.3f)" % current_pt
-            geo = flat_geometry[hits[0]]
-
-            # Determine which end of the path is closest.
-            distance2start = distance(current_pt, geo.coords[0])
-            distance2stop = distance(current_pt, geo.coords[-1])
-            print "  Path index =", hits[0]
-            print "  Start: ", "(%.3f, %.3f)" % geo.coords[0], "  D(Start): %.3f" % distance2start
-            print "  Stop : ", "(%.3f, %.3f)" % geo.coords[-1], "  D(Stop): %.3f" % distance2stop
-
-            # Reverse if end is closest.
-            if distance2start > distance2stop:
-                print "  Reversing!"
-                geo.coords = list(geo.coords)[::-1]
-
-            # G-code
-            if type(geo) == LineString or type(geo) == LinearRing:
-                self.gcode += self.linear2gcode(geo, tolerance=tolerance)
-            elif type(geo) == Point:
-                self.gcode += self.point2gcode(geo)
-            else:
-                log.warning("G-code generation not implemented for %s" % (str(type(geo))))
+        pt, geo = storage.nearest(current_pt)
+        try:
+            while True:
+                path_count += 1
+                #print "Current: ", "(%.3f, %.3f)" % current_pt
+
+                # Remove before modifying, otherwise
+                # deletion will fail.
+                storage.remove(geo)
+
+                if list(pt) == list(geo.coords[-1]):
+                    #print "Reversing"
+                    geo.coords = list(geo.coords)[::-1]
+
+                # G-code
+                if type(geo) == LineString or type(geo) == LinearRing:
+                    self.gcode += self.linear2gcode(geo, tolerance=tolerance)
+                elif type(geo) == Point:
+                    self.gcode += self.point2gcode(geo)
+                else:
+                    log.warning("G-code generation not implemented for %s" % (str(type(geo))))
+
+                # Delete from index, update current location and continue.
+                #rti.delete(hits[0], geo.coords[0])
+                #rti.delete(hits[0], geo.coords[-1])
+
+                current_pt = geo.coords[-1]
+
+                # Next
+                pt, geo = storage.nearest(current_pt)
 
-            # Delete from index, update current location and continue.
-            rti.delete(hits[0], geo.coords[0])
-            rti.delete(hits[0], geo.coords[-1])
-            current_pt = geo.coords[-1]
-            hits = list(rti.nearest(current_pt, 1))
+        except StopIteration:  # Nothing found in storage.
+            pass
 
         log.debug("%s paths traced." % path_count)
 
@@ -3188,11 +3250,21 @@ def distance(pt1, pt2):
 
 
 class FlatCAMRTree(object):
+
     def __init__(self):
+        # Python RTree Index
         self.rti = rtindex.Index()
+
+        ## Track object-point relationship
+        # Each is list of points in object.
         self.obj2points = []
+
+        # Index is index in rtree, value is index of
+        # object in obj2points.
         self.points2obj = []
 
+        self.get_points = lambda go: go.coords
+
     def grow_obj2points(self, idx):
         if len(self.obj2points) > idx:
             # len == 2, idx == 1, ok.
@@ -3207,15 +3279,14 @@ class FlatCAMRTree(object):
         self.grow_obj2points(objid)
         self.obj2points[objid] = []
 
-        for pt in obj.coords:
+        for pt in self.get_points(obj):
             self.rti.insert(len(self.points2obj), (pt[0], pt[1], pt[0], pt[1]), obj=objid)
             self.obj2points[objid].append(len(self.points2obj))
             self.points2obj.append(objid)
 
     def remove_obj(self, objid, obj):
         # Use all ptids to delete from index
-        for i in range(len(self.obj2points[objid])):
-            pt = obj.coords[i]
+        for i, pt in enumerate(self.get_points(obj)):
             self.rti.delete(self.obj2points[objid][i], (pt[0], pt[1], pt[0], pt[1]))
 
     def nearest(self, pt):
@@ -3233,17 +3304,32 @@ class FlatCAMRTreeStorage(FlatCAMRTree):
         super(FlatCAMRTreeStorage, self).insert(len(self.objects) - 1, obj)
 
     def remove(self, obj):
+        # Get index in list
         objidx = self.objects.index(obj)
+
+        # Remove from list
         self.objects[objidx] = None
+
+        # Remove from index
         self.remove_obj(objidx, obj)
 
     def get_objects(self):
         return (o for o in self.objects if o is not None)
 
     def nearest(self, pt):
+        """
+        Returns the nearest matching points and the object
+        it belongs to.
+
+        :param pt: Query point.
+        :return: (match_x, match_y), Object owner of
+          matching point.
+        :rtype: tuple
+        """
         tidx = super(FlatCAMRTreeStorage, self).nearest(pt)
         return (tidx.bbox[0], tidx.bbox[1]), self.objects[tidx.object]
 
+
 class myO:
     def __init__(self, coords):
         self.coords = coords

+ 12 - 3
doc/source/planning.rst

@@ -12,15 +12,17 @@ Drawing
 * Force perpendicular
 * Un-group (Union creates group)
 * Group (But not union)
-* Remove from index (rebuild index or make deleted instances
+* [DONE] Remove from index (rebuild index or make deleted instances
   equal to None in the list).
 * Better handling/abstraction of geometry types and lists of such.
+  * Plotting and extraction of point is now done in a quite
+    efficient recursive way.
 
 
 Algorithms
 ----------
 
-* Reverse path if end is nearer.
+* [DONE] Reverse path if end is nearer.
 * Seed paint: Specify seed.
 
 
@@ -48,4 +50,11 @@ Bugs
 ----
 
 * Unit conversion on opening.
-* `cascaded_union([])` bug requires more testing.
+* [DONE] `cascaded_union([])` bug requires more testing.
+  * Old version of GEOS
+
+
+Other
+-----
+
+* Unit testing

BIN
share/deleteshape16.png


BIN
share/deleteshape24.png


BIN
share/deleteshape32.png


+ 37 - 0
tests/test_fcrts.py

@@ -0,0 +1,37 @@
+from camlib import *
+from shapely.geometry import LineString, LinearRing
+
+s = FlatCAMRTreeStorage()
+
+geoms = [
+    LinearRing(((0.5699056603773586, 0.7216037735849057),
+                (0.9885849056603774, 0.7216037735849057),
+                (0.9885849056603774, 0.6689622641509434),
+                (0.5699056603773586, 0.6689622641509434),
+                (0.5699056603773586, 0.7216037735849057))),
+    LineString(((0.8684952830188680, 0.6952830188679245),
+                (0.8680655198743615, 0.6865349890935113),
+                (0.8667803692948564, 0.6778712076279851),
+                (0.8646522079829676, 0.6693751114229638),
+                (0.8645044888670096, 0.6689622641509434))),
+    LineString(((0.9874952830188680, 0.6952830188679245),
+                (0.9864925023483531, 0.6748709493942936),
+                (0.9856160316877274, 0.6689622641509434))),
+
+]
+
+for geo in geoms:
+    s.insert(geo)
+
+current_pt = (0, 0)
+pt, geo = s.nearest(current_pt)
+while geo is not None:
+    print pt, geo
+    print "OBJECTS BEFORE:", s.objects
+
+    #geo.coords = list(geo.coords[::-1])
+    s.remove(geo)
+
+    print "OBJECTS AFTER:", s.objects
+    current_pt = geo.coords[-1]
+    pt, geo = s.nearest(current_pt)