Browse Source

"Paint connect" functional. Added to seed-based painting and unit-tested. "Path connect" still pending. Disabled. "Path connect" unit test added.

jpcaram 11 năm trước cách đây
mục cha
commit
6b51f03db2
5 tập tin đã thay đổi với 381 bổ sung81 xóa
  1. 2 0
      FlatCAMObj.py
  2. 109 81
      camlib.py
  3. 197 0
      tests/test_paint.py
  4. 27 0
      tests/test_pathconnect.py
  5. 46 0
      tests/test_plotg.py

+ 2 - 0
FlatCAMObj.py

@@ -963,6 +963,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
 
 
         # To be called after clicking on the plot.
         # To be called after clicking on the plot.
         def doit(event):
         def doit(event):
+            self.app.info("Painting polygon...")
             self.app.plotcanvas.mpl_disconnect(subscription)
             self.app.plotcanvas.mpl_disconnect(subscription)
             point = [event.xdata, event.ydata]
             point = [event.xdata, event.ydata]
             self.paint_poly(point, tooldia, overlap)
             self.paint_poly(point, tooldia, overlap)
@@ -991,6 +992,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                 cp = self.clear_polygon2(poly.buffer(-self.options["paintmargin"]), tooldia, overlap=overlap)
                 cp = self.clear_polygon2(poly.buffer(-self.options["paintmargin"]), tooldia, overlap=overlap)
             geo_obj.solid_geometry = cp
             geo_obj.solid_geometry = cp
             geo_obj.options["cnctooldia"] = tooldia
             geo_obj.options["cnctooldia"] = tooldia
+            self.app.inform.emit("Done.")
 
 
         name = self.options["name"] + "_paint"
         name = self.options["name"] + "_paint"
         self.app.new_object("geometry", name, gen_paintarea)
         self.app.new_object("geometry", name, gen_paintarea)

+ 109 - 81
camlib.py

@@ -37,7 +37,7 @@ from descartes.patch import PolygonPatch
 
 
 import simplejson as json
 import simplejson as json
 # TODO: Commented for FlatCAM packaging with cx_freeze
 # TODO: Commented for FlatCAM packaging with cx_freeze
-#from matplotlib.pyplot import plot
+from matplotlib.pyplot import plot, subplot
 
 
 import logging
 import logging
 
 
@@ -388,12 +388,21 @@ class Geometry(object):
                 inner_edges.append(y)
                 inner_edges.append(y)
         geoms += outer_edges + inner_edges
         geoms += outer_edges + inner_edges
 
 
-        # Optimization
+        # Optimization: Join paths
         # TODO: Re-architecture?
         # TODO: Re-architecture?
+        # log.debug("Simplifying paths...")
         g = Geometry()
         g = Geometry()
         g.solid_geometry = geoms
         g.solid_geometry = geoms
-        g.path_connect()
-        return g.flat_geometry
+        # g.path_connect()
+        #return g.flat_geometry
+
+        g.flatten(pathonly=True)
+
+        # Optimization: Reduce lifts
+        log.debug("Reducing tool lifts...")
+        p = self.paint_connect(g.flat_geometry, polygon, tooldia)
+
+        return p
 
 
         #return geoms
         #return geoms
 
 
@@ -418,7 +427,8 @@ class Geometry(object):
         """
         """
         return
         return
 
 
-    def paint_connect(self, geolist, boundary, tooldia):
+    @staticmethod
+    def paint_connect(geolist, boundary, tooldia):
         """
         """
         Connects paths that results in a connection segment that is
         Connects paths that results in a connection segment that is
         within the paint area. This avoids unnecessary tool lifting.
         within the paint area. This avoids unnecessary tool lifting.
@@ -437,17 +447,20 @@ class Geometry(object):
 
 
         for shape in geolist:
         for shape in geolist:
             if shape is not None:  # TODO: This shouldn't have happened.
             if shape is not None:  # TODO: This shouldn't have happened.
-                storage.insert(shape)
+                # Make LlinearRings into linestrings otherwise
+                # When chaining the coordinates path is messed up.
+                storage.insert(LineString(shape))
+                #storage.insert(shape)
 
 
         ## Iterate over geometry paths getting the nearest each time.
         ## Iterate over geometry paths getting the nearest each time.
         optimized_paths = []
         optimized_paths = []
-        temp_path = None
         path_count = 0
         path_count = 0
         current_pt = (0, 0)
         current_pt = (0, 0)
         pt, geo = storage.nearest(current_pt)
         pt, geo = storage.nearest(current_pt)
         try:
         try:
             while True:
             while True:
                 path_count += 1
                 path_count += 1
+                log.debug("Path %d" % path_count)
 
 
                 # Remove before modifying, otherwise
                 # Remove before modifying, otherwise
                 # deletion will fail.
                 # deletion will fail.
@@ -461,27 +474,35 @@ class Geometry(object):
                 # Straight line from current_pt to pt.
                 # Straight line from current_pt to pt.
                 # Is the toolpath inside the geometry?
                 # Is the toolpath inside the geometry?
                 jump = LineString([current_pt, pt]).buffer(tooldia / 2)
                 jump = LineString([current_pt, pt]).buffer(tooldia / 2)
+
                 if jump.within(boundary):
                 if jump.within(boundary):
+                    log.debug("Jump to path #%d is inside. Joining." % path_count)
+
                     # Completely inside. Append...
                     # Completely inside. Append...
-                    if temp_path is None:
-                        temp_path = geo
-                    else:
-                        temp_path.coords = list(temp_path.coords) + list(geo.coords)
+                    try:
+                        last = optimized_paths[-1]
+                        last.coords = list(last.coords) + list(geo.coords)
+                    except IndexError:
+                        optimized_paths.append(geo)
+
                 else:
                 else:
+
                     # Have to lift tool. End path.
                     # Have to lift tool. End path.
-                    optimized_paths.append(temp_path)
-                    temp_path = geo
+                    log.debug("Path #%d not within boundary. Next." % path_count)
+                    optimized_paths.append(geo)
 
 
                 current_pt = geo.coords[-1]
                 current_pt = geo.coords[-1]
 
 
                 # Next
                 # Next
                 pt, geo = storage.nearest(current_pt)
                 pt, geo = storage.nearest(current_pt)
 
 
-        except StopIteration:  # Nothing found in storage.
-            if not temp_path.equals(optimized_paths[-1]):
-                optimized_paths.append(temp_path)
+        except StopIteration:  # Nothing left in storage.
+            pass
+
+        return optimized_paths
 
 
-    def path_connect(self):
+    @staticmethod
+    def path_connect(pathlist):
         """
         """
         Simplifies a list of paths by joining those whose ends touch.
         Simplifies a list of paths by joining those whose ends touch.
         The list of paths of generated from the geometry.flatten()
         The list of paths of generated from the geometry.flatten()
@@ -491,7 +512,7 @@ class Geometry(object):
         :return: None
         :return: None
         """
         """
 
 
-        flat_geometry = self.flatten(pathonly=True)
+        # flat_geometry = self.flatten(pathonly=True)
 
 
         ## Index first and last points in paths
         ## Index first and last points in paths
         def get_pts(o):
         def get_pts(o):
@@ -500,7 +521,7 @@ class Geometry(object):
         storage = FlatCAMRTreeStorage()
         storage = FlatCAMRTreeStorage()
         storage.get_points = get_pts
         storage.get_points = get_pts
 
 
-        for shape in flat_geometry:
+        for shape in pathlist:
             if shape is not None:  # TODO: This shouldn't have happened.
             if shape is not None:  # TODO: This shouldn't have happened.
                 storage.insert(shape)
                 storage.insert(shape)
 
 
@@ -558,7 +579,8 @@ class Geometry(object):
         except StopIteration:  # Nothing found in storage.
         except StopIteration:  # Nothing found in storage.
             pass
             pass
 
 
-        self.flat_geometry = optimized_geometry
+        #self.flat_geometry = optimized_geometry
+        return optimized_geometry
 
 
     def convert_units(self, units):
     def convert_units(self, units):
         """
         """
@@ -2435,6 +2457,7 @@ class CNCjob(Geometry):
         :return: None
         :return: None
         """
         """
         assert isinstance(geometry, Geometry)
         assert isinstance(geometry, Geometry)
+        log.debug("generate_from_geometry_2()")
 
 
         ## Flatten the geometry
         ## Flatten the geometry
         # Only linear elements (no polygons) remain.
         # Only linear elements (no polygons) remain.
@@ -2442,12 +2465,16 @@ class CNCjob(Geometry):
         log.debug("%d paths" % len(flat_geometry))
         log.debug("%d paths" % len(flat_geometry))
 
 
         ## Index first and last points in paths
         ## Index first and last points in paths
+        # What points to index.
         def get_pts(o):
         def get_pts(o):
             return [o.coords[0], o.coords[-1]]
             return [o.coords[0], o.coords[-1]]
 
 
+        # Create the indexed storage.
         storage = FlatCAMRTreeStorage()
         storage = FlatCAMRTreeStorage()
         storage.get_points = get_pts
         storage.get_points = get_pts
 
 
+        # Store the geometry
+        log.debug("Indexing geometry before generating G-Code...")
         for shape in flat_geometry:
         for shape in flat_geometry:
             if shape is not None:  # TODO: This shouldn't have happened.
             if shape is not None:  # TODO: This shouldn't have happened.
                 storage.insert(shape)
                 storage.insert(shape)
@@ -2455,7 +2482,7 @@ class CNCjob(Geometry):
         if tooldia is not None:
         if tooldia is not None:
             self.tooldia = tooldia
             self.tooldia = tooldia
 
 
-        self.input_geometry_bounds = geometry.bounds()
+        # self.input_geometry_bounds = geometry.bounds()
 
 
         if not append:
         if not append:
             self.gcode = ""
             self.gcode = ""
@@ -2470,6 +2497,7 @@ class CNCjob(Geometry):
         self.gcode += self.pausecode + "\n"
         self.gcode += self.pausecode + "\n"
 
 
         ## Iterate over geometry paths getting the nearest each time.
         ## Iterate over geometry paths getting the nearest each time.
+        log.debug("Starting G-Code...")
         path_count = 0
         path_count = 0
         current_pt = (0, 0)
         current_pt = (0, 0)
         pt, geo = storage.nearest(current_pt)
         pt, geo = storage.nearest(current_pt)
@@ -2997,7 +3025,7 @@ def dict2obj(d):
         return d
         return d
 
 
 
 
-def plotg(geo, solid_poly=False):
+def plotg(geo, solid_poly=False, color="black"):
     try:
     try:
         _ = iter(geo)
         _ = iter(geo)
     except:
     except:
@@ -3015,15 +3043,15 @@ def plotg(geo, solid_poly=False):
                 ax.add_patch(patch)
                 ax.add_patch(patch)
             else:
             else:
                 x, y = g.exterior.coords.xy
                 x, y = g.exterior.coords.xy
-                plot(x, y)
+                plot(x, y, color=color)
                 for ints in g.interiors:
                 for ints in g.interiors:
                     x, y = ints.coords.xy
                     x, y = ints.coords.xy
-                    plot(x, y)
+                    plot(x, y, color=color)
                 continue
                 continue
 
 
         if type(g) == LineString or type(g) == LinearRing:
         if type(g) == LineString or type(g) == LinearRing:
             x, y = g.coords.xy
             x, y = g.coords.xy
-            plot(x, y)
+            plot(x, y, color=color)
             continue
             continue
 
 
         if type(g) == Point:
         if type(g) == Point:
@@ -3033,7 +3061,7 @@ def plotg(geo, solid_poly=False):
 
 
         try:
         try:
             _ = iter(g)
             _ = iter(g)
-            plotg(g)
+            plotg(g, color=color)
         except:
         except:
             log.error("Cannot plot: " + str(type(g)))
             log.error("Cannot plot: " + str(type(g)))
             continue
             continue
@@ -3380,58 +3408,58 @@ class FlatCAMRTreeStorage(FlatCAMRTree):
         return (tidx.bbox[0], tidx.bbox[1]), self.objects[tidx.object]
         return (tidx.bbox[0], tidx.bbox[1]), self.objects[tidx.object]
 
 
 
 
-class myO:
-    def __init__(self, coords):
-        self.coords = coords
-
-
-def test_rti():
-
-    o1 = myO([(0, 0), (0, 1), (1, 1)])
-    o2 = myO([(2, 0), (2, 1), (2, 1)])
-    o3 = myO([(2, 0), (2, 1), (3, 1)])
-
-    os = [o1, o2]
-
-    idx = FlatCAMRTree()
-
-    for o in range(len(os)):
-        idx.insert(o, os[o])
-
-    print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)]
-
-    idx.remove_obj(0, o1)
-
-    print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)]
-
-    idx.remove_obj(1, o2)
-
-    print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)]
-
-
-def test_rtis():
-
-    o1 = myO([(0, 0), (0, 1), (1, 1)])
-    o2 = myO([(2, 0), (2, 1), (2, 1)])
-    o3 = myO([(2, 0), (2, 1), (3, 1)])
-
-    os = [o1, o2]
-
-    idx = FlatCAMRTreeStorage()
-
-    for o in range(len(os)):
-        idx.insert(os[o])
-
-    #os = None
-    #o1 = None
-    #o2 = None
-
-    print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)]
-
-    idx.remove(idx.nearest((2,0))[1])
-
-    print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)]
-
-    idx.remove(idx.nearest((0,0))[1])
-
-    print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)]
+# class myO:
+#     def __init__(self, coords):
+#         self.coords = coords
+#
+#
+# def test_rti():
+#
+#     o1 = myO([(0, 0), (0, 1), (1, 1)])
+#     o2 = myO([(2, 0), (2, 1), (2, 1)])
+#     o3 = myO([(2, 0), (2, 1), (3, 1)])
+#
+#     os = [o1, o2]
+#
+#     idx = FlatCAMRTree()
+#
+#     for o in range(len(os)):
+#         idx.insert(o, os[o])
+#
+#     print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)]
+#
+#     idx.remove_obj(0, o1)
+#
+#     print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)]
+#
+#     idx.remove_obj(1, o2)
+#
+#     print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)]
+#
+#
+# def test_rtis():
+#
+#     o1 = myO([(0, 0), (0, 1), (1, 1)])
+#     o2 = myO([(2, 0), (2, 1), (2, 1)])
+#     o3 = myO([(2, 0), (2, 1), (3, 1)])
+#
+#     os = [o1, o2]
+#
+#     idx = FlatCAMRTreeStorage()
+#
+#     for o in range(len(os)):
+#         idx.insert(os[o])
+#
+#     #os = None
+#     #o1 = None
+#     #o2 = None
+#
+#     print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)]
+#
+#     idx.remove(idx.nearest((2,0))[1])
+#
+#     print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)]
+#
+#     idx.remove(idx.nearest((0,0))[1])
+#
+#     print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)]

+ 197 - 0
tests/test_paint.py

@@ -0,0 +1,197 @@
+import unittest
+
+from shapely.geometry import LineString, Polygon
+from shapely.ops import cascaded_union, unary_union
+from matplotlib.pyplot import plot, subplot, show, cla, clf, xlim, ylim, title
+from camlib import *
+
+
+def plotg2(geo, solid_poly=False, color="black", linestyle='solid'):
+
+    try:
+        for sub_geo in geo:
+            plotg2(sub_geo, solid_poly=solid_poly, color=color, linestyle=linestyle)
+    except TypeError:
+        if type(geo) == Polygon:
+            if solid_poly:
+                patch = PolygonPatch(geo,
+                                     #facecolor="#BBF268",
+                                     facecolor=color,
+                                     edgecolor="#006E20",
+                                     alpha=0.5,
+                                     zorder=2)
+                ax = subplot(111)
+                ax.add_patch(patch)
+            else:
+                x, y = geo.exterior.coords.xy
+                plot(x, y, color=color, linestyle=linestyle)
+                for ints in geo.interiors:
+                    x, y = ints.coords.xy
+                    plot(x, y, color=color, linestyle=linestyle)
+
+        if type(geo) == LineString or type(geo) == LinearRing:
+            x, y = geo.coords.xy
+            plot(x, y, color=color, linestyle=linestyle)
+
+        if type(geo) == Point:
+            x, y = geo.coords.xy
+            plot(x, y, 'o')
+
+
+class PaintTestCase(unittest.TestCase):
+    # def __init__(self):
+    #     super(PaintTestCase, self).__init__()
+    #     self.boundary = None
+    #     self.descr = None
+
+    def plot_summary_A(self, paths, tooldia, result, msg):
+        plotg2(self.boundary, solid_poly=True, color="green")
+        plotg2(paths, color="red")
+        plotg2([r.buffer(tooldia / 2) for r in result], solid_poly=True, color="blue")
+        plotg2(result, color="black", linestyle='dashed')
+        title(msg)
+        xlim(0, 5)
+        ylim(0, 5)
+        show()
+
+
+class PaintConnectTest(PaintTestCase):
+    """
+    Simple rectangular boundary and paths inside.
+    """
+
+    def setUp(self):
+        self.boundary = Polygon([[0, 0], [0, 5], [5, 5], [5, 0]])
+
+    def test_jump(self):
+        print "Test: WALK Expected"
+        paths = [
+            LineString([[0.5, 2], [2, 4.5]]),
+            LineString([[2, 0.5], [4.5, 2]])
+        ]
+        for p in paths:
+            print p
+
+        tooldia = 1.0
+
+        print "--"
+        result = Geometry.paint_connect(paths, self.boundary, tooldia)
+        for r in result:
+            print r
+
+        self.assertEqual(len(result), 1)
+
+        # plotg(self.boundary, solid_poly=True)
+        # plotg(paths, color="red")
+        # plotg([r.buffer(tooldia / 2) for r in result], solid_poly=True)
+        # show()
+        # #cla()
+        # clf()
+
+        self.plot_summary_A(paths, tooldia, result, "WALK expected.")
+
+    def test_no_jump1(self):
+        print "Test: FLY Expected"
+        paths = [
+            LineString([[0, 2], [2, 5]]),
+            LineString([[2, 0], [5, 2]])
+        ]
+        for p in paths:
+            print p
+
+        tooldia = 1.0
+
+        print "--"
+        result = Geometry.paint_connect(paths, self.boundary, tooldia)
+        for r in result:
+            print r
+
+        self.assertEqual(len(result), len(paths))
+
+        self.plot_summary_A(paths, tooldia, result, "FLY Expected")
+
+    def test_no_jump2(self):
+        print "Test: FLY Expected"
+        paths = [
+            LineString([[0.5, 2], [2, 4.5]]),
+            LineString([[2, 0.5], [4.5, 2]])
+        ]
+        for p in paths:
+            print p
+
+        tooldia = 1.1
+
+        print "--"
+        result = Geometry.paint_connect(paths, self.boundary, tooldia)
+        for r in result:
+            print r
+
+        self.assertEqual(len(result), len(paths))
+
+        self.plot_summary_A(paths, tooldia, result, "FLY Expected")
+
+
+class PaintConnectTest2(PaintTestCase):
+    """
+    Boundary with an internal cutout.
+    """
+
+    def setUp(self):
+        self.boundary = Polygon([[0, 0], [0, 5], [5, 5], [5, 0]])
+        self.boundary = self.boundary.difference(
+            Polygon([[2, 1], [3, 1], [3, 4], [2, 4]])
+        )
+
+    def test_no_jump3(self):
+        print "TEST: No jump expected"
+        paths = [
+            LineString([[0.5, 1], [1.5, 3]]),
+            LineString([[4, 1], [4, 4]])
+        ]
+        for p in paths:
+            print p
+
+        tooldia = 1.0
+
+        print "--"
+        result = Geometry.paint_connect(paths, self.boundary, tooldia)
+        for r in result:
+            print r
+
+        self.assertEqual(len(result), len(paths))
+
+        self.plot_summary_A(paths, tooldia, result, "FLY Expected")
+
+
+class PaintConnectTest3(PaintTestCase):
+    """
+    Tests with linerings among elements.
+    """
+
+    def setUp(self):
+        self.boundary = Polygon([[0, 0], [0, 5], [5, 5], [5, 0]])
+
+    def test_jump2(self):
+        print "Test: WALK Expected"
+        paths = [
+            LineString([[0.5, 2], [2, 4.5]]),
+            LineString([[2, 0.5], [4.5, 2]]),
+            self.boundary.buffer(-0.5).exterior
+        ]
+        for p in paths:
+            print p
+
+        tooldia = 1.0
+
+        print "--"
+        result = Geometry.paint_connect(paths, self.boundary, tooldia)
+        for r in result:
+            print r
+
+        self.assertEqual(len(result), 1)
+
+        self.plot_summary_A(paths, tooldia, result, "WALK Expected")
+
+
+if __name__ == '__main__':
+    unittest.main()

+ 27 - 0
tests/test_pathconnect.py

@@ -0,0 +1,27 @@
+import unittest
+
+from shapely.geometry import LineString, Polygon
+from shapely.ops import cascaded_union, unary_union
+from matplotlib.pyplot import plot, subplot, show, cla, clf, xlim, ylim, title
+from camlib import *
+
+
+class PathConnectTest1(unittest.TestCase):
+
+    def setUp(self):
+        pass
+
+    def test_simple_connect(self):
+        paths = [
+            LineString([[0, 0], [0, 1]]),
+            LineString([[0, 1], [0, 2]])
+        ]
+
+        result = Geometry.path_connect(paths)
+
+        self.assertEqual(len(result), 1)
+        self.assertTrue(result[0].equals(LineString([[0, 0], [0, 2]])))
+
+
+if __name__ == "__main__":
+    unittest.main()

+ 46 - 0
tests/test_plotg.py

@@ -0,0 +1,46 @@
+from shapely.geometry import LineString, Polygon
+from shapely.ops import cascaded_union, unary_union
+from matplotlib.pyplot import plot, subplot, show
+from camlib import *
+
+def plotg2(geo, solid_poly=False, color="black", linestyle='solid'):
+
+    try:
+        for sub_geo in geo:
+            plotg2(sub_geo, solid_poly=solid_poly, color=color, linestyle=linestyle)
+    except TypeError:
+        if type(geo) == Polygon:
+            if solid_poly:
+                patch = PolygonPatch(geo,
+                                     #facecolor="#BBF268",
+                                     facecolor=color,
+                                     edgecolor="#006E20",
+                                     alpha=0.5,
+                                     zorder=2)
+                ax = subplot(111)
+                ax.add_patch(patch)
+            else:
+                x, y = geo.exterior.coords.xy
+                plot(x, y, color=color, linestyle=linestyle)
+                for ints in geo.interiors:
+                    x, y = ints.coords.xy
+                    plot(x, y, color=color, linestyle=linestyle)
+
+        if type(geo) == LineString or type(geo) == LinearRing:
+            x, y = geo.coords.xy
+            plot(x, y, color=color, linestyle=linestyle)
+
+        if type(geo) == Point:
+            x, y = geo.coords.xy
+            plot(x, y, 'o')
+
+
+if __name__ == "__main__":
+    p = Polygon([[0, 0], [0, 5], [5, 5], [5, 0]])
+    paths = [
+        LineString([[0.5, 2], [2, 4.5]]),
+        LineString([[2, 0.5], [4.5, 2]])
+    ]
+    plotg2(p, solid_poly=True)
+    plotg2(paths, linestyle="dashed")
+    show()