Przeglądaj źródła

Refactored paint_connect() and path_connect() to receive and return FlatCAMRTreeStorage objects. Updated unittests acordingly.

jpcaram 11 lat temu
rodzic
commit
6733ebbfa8
4 zmienionych plików z 131 dodań i 84 usunięć
  1. 6 0
      FlatCAMApp.py
  2. 80 68
      camlib.py
  3. 26 12
      tests/test_paint.py
  4. 19 4
      tests/test_pathconnect.py

+ 6 - 0
FlatCAMApp.py

@@ -1565,6 +1565,12 @@ class App(QtCore.QObject):
                 app_obj.inform.emit("[error] Failed to open file: " + filename)
                 app_obj.progress.emit(0)
                 raise IOError('Failed to open file: ' + filename)
+            except ParseError, e:
+                app_obj.inform.emit("[error] Failed to parse file: " + filename)
+                app_obj.progress.emit(0)
+                self.log.error(str(e))
+                raise
+                return
 
             # Further parsing
             self.progress.emit(70)  # TODO: Note the mixture of self and app_obj used here

+ 80 - 68
camlib.py

@@ -6,6 +6,7 @@
 # MIT Licence                                              #
 ############################################################
 #from __future__ import division
+from scipy import optimize
 import traceback
 
 from numpy import arctan2, Inf, array, sqrt, pi, ceil, sin, cos, dot, float32, \
@@ -337,7 +338,8 @@ class Geometry(object):
                 break
         return poly_cuts
 
-    def clear_polygon2(self, polygon, tooldia, seedpoint=None, overlap=0.15):
+    @staticmethod
+    def clear_polygon2(polygon, tooldia, seedpoint=None, overlap=0.15):
         """
         Creates geometry inside a polygon for a tool to cover
         the whole area.
@@ -357,8 +359,12 @@ class Geometry(object):
         # Current buffer radius
         radius = tooldia / 2 * (1 - overlap)
 
-        # The toolpaths
-        geoms = []
+        ## The toolpaths
+        # Index first and last points in paths
+        def get_pts(o):
+            return [o.coords[0], o.coords[-1]]
+        geoms = FlatCAMRTreeStorage()
+        geoms.get_points = get_pts
 
         # Path margin
         path_margin = polygon.buffer(-tooldia / 2)
@@ -367,7 +373,8 @@ class Geometry(object):
         if seedpoint is None:
             seedpoint = path_margin.representative_point()
 
-        # Grow from seed until outside the box.
+        # Grow from seed until outside the box. The polygons will
+        # never have an interior, so take the exterior LinearRing.
         while 1:
             path = Point(seedpoint).buffer(radius).exterior
             path = path.intersection(path_margin)
@@ -376,36 +383,30 @@ class Geometry(object):
             if path.is_empty:
                 break
             else:
-                geoms.append(path)
+                #geoms.append(path)
+                geoms.insert(path)
 
             radius += tooldia * (1 - overlap)
 
-        # Clean edges
+        # Clean inside edges of the original polygon
         outer_edges = [x.exterior for x in autolist(polygon.buffer(-tooldia / 2))]
         inner_edges = []
         for x in autolist(polygon.buffer(-tooldia / 2)):  # Over resulting polygons
             for y in x.interiors:  # Over interiors of each polygon
                 inner_edges.append(y)
-        geoms += outer_edges + inner_edges
+        #geoms += outer_edges + inner_edges
+        for g in outer_edges + inner_edges:
+            geoms.insert(g)
 
-        # Optimization: Join paths
-        # TODO: Re-architecture?
-        # log.debug("Simplifying paths...")
-        g = Geometry()
-        g.solid_geometry = geoms
-        # g.path_connect()
-        #return g.flat_geometry
+        # Optimization connect touching paths
 
-        g.flatten(pathonly=True)
 
         # Optimization: Reduce lifts
         log.debug("Reducing tool lifts...")
-        p = self.paint_connect(g.flat_geometry, polygon, tooldia)
+        p = Geometry.paint_connect(g.flat_geometry, polygon, tooldia)
 
         return p
 
-        #return geoms
-
     def scale(self, factor):
         """
         Scales all of the object's geometry by a given factor. Override
@@ -428,7 +429,7 @@ class Geometry(object):
         return
 
     @staticmethod
-    def paint_connect(geolist, boundary, tooldia):
+    def paint_connect(storage, boundary, tooldia):
         """
         Connects paths that results in a connection segment that is
         within the paint area. This avoids unnecessary tool lifting.
@@ -442,34 +443,39 @@ class Geometry(object):
         def get_pts(o):
             return [o.coords[0], o.coords[-1]]
 
-        storage = FlatCAMRTreeStorage()
-        storage.get_points = get_pts
-
-        for shape in geolist:
-            if shape is not None:  # TODO: This shouldn't have happened.
-                # Make LlinearRings into linestrings otherwise
-                # When chaining the coordinates path is messed up.
-                storage.insert(LineString(shape))
-                #storage.insert(shape)
+        # storage = FlatCAMRTreeStorage()
+        # storage.get_points = get_pts
+        #
+        # for shape in geolist:
+        #     if shape is not None:  # TODO: This shouldn't have happened.
+        #         # 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.
-        optimized_paths = []
+        #optimized_paths = []
+        optimized_paths = FlatCAMRTreeStorage()
+        optimized_paths.get_points = get_pts
         path_count = 0
         current_pt = (0, 0)
         pt, geo = storage.nearest(current_pt)
+        storage.remove(geo)
+        geo = LineString(geo)
+        current_pt = geo.coords[-1]
         try:
             while True:
                 path_count += 1
                 log.debug("Path %d" % path_count)
 
-                # Remove before modifying, otherwise
-                # deletion will fail.
-                storage.remove(geo)
+                pt, candidate = storage.nearest(current_pt)
+                storage.remove(candidate)
+                candidate = LineString(candidate)
 
                 # If last point in geometry is the nearest
                 # then reverse coordinates.
-                if list(pt) == list(geo.coords[-1]):
-                    geo.coords = list(geo.coords)[::-1]
+                if list(pt) == list(candidate.coords[-1]):
+                    candidate.coords = list(candidate.coords)[::-1]
 
                 # Straight line from current_pt to pt.
                 # Is the toolpath inside the geometry?
@@ -479,35 +485,35 @@ class Geometry(object):
                     log.debug("Jump to path #%d is inside. Joining." % path_count)
 
                     # Completely inside. Append...
-                    try:
-                        last = optimized_paths[-1]
-                        last.coords = list(last.coords) + list(geo.coords)
-                    except IndexError:
-                        optimized_paths.append(geo)
+                    geo.coords = list(geo.coords) + list(candidate.coords)
+                    # try:
+                    #     last = optimized_paths[-1]
+                    #     last.coords = list(last.coords) + list(geo.coords)
+                    # except IndexError:
+                    #     optimized_paths.append(geo)
 
                 else:
 
                     # Have to lift tool. End path.
                     log.debug("Path #%d not within boundary. Next." % path_count)
-                    optimized_paths.append(geo)
+                    #optimized_paths.append(geo)
+                    optimized_paths.insert(geo)
+                    geo = candidate
 
                 current_pt = geo.coords[-1]
 
                 # Next
-                pt, geo = storage.nearest(current_pt)
+                #pt, geo = storage.nearest(current_pt)
 
         except StopIteration:  # Nothing left in storage.
-            pass
+            #pass
+            optimized_paths.insert(geo)
 
         return optimized_paths
 
     @staticmethod
-    def path_connect(pathlist, origin=(0, 0)):
+    def path_connect(storage, origin=(0, 0)):
         """
-        Simplifies a list of paths by joining those whose ends touch.
-        The list of paths of generated from the geometry.flatten()
-        method which writes to geometry.flat_geometry. This list
-        if overwritten by this method with the optimized result.
 
         :return: None
         """
@@ -515,18 +521,21 @@ class Geometry(object):
         ## 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 pathlist:
-            if shape is not None:  # TODO: This shouldn't have happened.
-                storage.insert(shape)
+        #
+        # storage = FlatCAMRTreeStorage()
+        # storage.get_points = get_pts
+        #
+        # for shape in pathlist:
+        #     if shape is not None:  # TODO: This shouldn't have happened.
+        #         storage.insert(shape)
 
         path_count = 0
         pt, geo = storage.nearest(origin)
         storage.remove(geo)
-        optimized_geometry = [geo]
+        #optimized_geometry = [geo]
+        optimized_geometry = FlatCAMRTreeStorage()
+        optimized_geometry.get_points = get_pts
+        #optimized_geometry.insert(geo)
         try:
             while True:
                 path_count += 1
@@ -536,6 +545,8 @@ class Geometry(object):
                 _, left = storage.nearest(geo.coords[0])
                 print "left is", left
 
+                # If left touches geo, remove left from original
+                # storage and append to geo.
                 if type(left) == LineString:
                     if left.coords[0] == geo.coords[0]:
                         storage.remove(left)
@@ -560,6 +571,8 @@ class Geometry(object):
                 _, right = storage.nearest(geo.coords[-1])
                 print "right is", right
 
+                # If right touches geo, remove left from original
+                # storage and append to geo.
                 if type(right) == LineString:
                     if right.coords[0] == geo.coords[-1]:
                         storage.remove(right)
@@ -581,23 +594,22 @@ class Geometry(object):
                         geo.coords = list(left.coords) + list(geo.coords)
                         continue
 
-                # No matches on either end
-                #optimized_geometry.append(geo)
-
+                # right is either a LinearRing or it does not connect
+                # to geo (nothing left to connect to geo), so we continue
+                # with right as geo.
                 storage.remove(right)
+
                 if type(right) == LinearRing:
-                    # Put the linearring at the beginning so it does
-                    # not iterfere.
-                    optimized_geometry = [right] + optimized_geometry
-                    geo = optimized_geometry[-1]
-                    print "right was LinearRing, getting previous"
+                    optimized_geometry.insert(right)
                 else:
-                    optimized_geometry.append(right)
+                    # Cannot exteng geo any further. Put it away.
+                    optimized_geometry.insert(geo)
+
+                    # Continue with right.
                     geo = right
-                    print "stored right, now geo<-right"
 
         except StopIteration:  # Nothing found in storage.
-            pass
+            optimized_geometry.insert(geo)
 
         print path_count
 
@@ -1838,7 +1850,7 @@ class Gerber (Geometry):
 
                 ## --- Buffered ---
                 width = self.apertures[last_path_aperture]["size"]
-                geo = LineString(path).buffer(width/2)
+                geo = LineString(path).buffer(width / 2)
                 poly_buffer.append(geo)
 
             # --- Apply buffer ---
@@ -1850,7 +1862,7 @@ class Gerber (Geometry):
         except Exception, err:
             #print traceback.format_exc()
             log.error("PARSING FAILED. Line %d: %s" % (line_num, gline))
-            raise
+            raise ParseError("%s\nLine %d: %s" % (repr(err), line_num, gline))
 
     @staticmethod
     def create_flash_geometry(location, aperture):

+ 26 - 12
tests/test_paint.py

@@ -4,6 +4,16 @@ 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 *
+from copy import deepcopy
+
+def mkstorage(paths):
+    def get_pts(o):
+        return [o.coords[0], o.coords[-1]]
+    storage = FlatCAMRTreeStorage()
+    storage.get_points = get_pts
+    for p in paths:
+        storage.insert(p)
+    return storage
 
 
 def plotg2(geo, solid_poly=False, color="black", linestyle='solid'):
@@ -75,19 +85,14 @@ class PaintConnectTest(PaintTestCase):
         tooldia = 1.0
 
         print "--"
-        result = Geometry.paint_connect(paths, self.boundary, tooldia)
+        result = Geometry.paint_connect(mkstorage(deepcopy(paths)), self.boundary, tooldia)
+
+        result = list(result.get_objects())
         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):
@@ -102,7 +107,9 @@ class PaintConnectTest(PaintTestCase):
         tooldia = 1.0
 
         print "--"
-        result = Geometry.paint_connect(paths, self.boundary, tooldia)
+        result = Geometry.paint_connect(mkstorage(deepcopy(paths)), self.boundary, tooldia)
+
+        result = list(result.get_objects())
         for r in result:
             print r
 
@@ -122,7 +129,9 @@ class PaintConnectTest(PaintTestCase):
         tooldia = 1.1
 
         print "--"
-        result = Geometry.paint_connect(paths, self.boundary, tooldia)
+        result = Geometry.paint_connect(mkstorage(deepcopy(paths)), self.boundary, tooldia)
+
+        result = list(result.get_objects())
         for r in result:
             print r
 
@@ -154,7 +163,9 @@ class PaintConnectTest2(PaintTestCase):
         tooldia = 1.0
 
         print "--"
-        result = Geometry.paint_connect(paths, self.boundary, tooldia)
+        result = Geometry.paint_connect(mkstorage(deepcopy(paths)), self.boundary, tooldia)
+
+        result = list(result.get_objects())
         for r in result:
             print r
 
@@ -170,6 +181,7 @@ class PaintConnectTest3(PaintTestCase):
 
     def setUp(self):
         self.boundary = Polygon([[0, 0], [0, 5], [5, 5], [5, 0]])
+        print "TEST w/ LinearRings"
 
     def test_jump2(self):
         print "Test: WALK Expected"
@@ -184,7 +196,9 @@ class PaintConnectTest3(PaintTestCase):
         tooldia = 1.0
 
         print "--"
-        result = Geometry.paint_connect(paths, self.boundary, tooldia)
+        result = Geometry.paint_connect(mkstorage(deepcopy(paths)), self.boundary, tooldia)
+
+        result = list(result.get_objects())
         for r in result:
             print r
 

+ 19 - 4
tests/test_pathconnect.py

@@ -7,9 +7,20 @@ from camlib import *
 from random import random
 
 
+def mkstorage(paths):
+    def get_pts(o):
+        return [o.coords[0], o.coords[-1]]
+    storage = FlatCAMRTreeStorage()
+    storage.get_points = get_pts
+    for p in paths:
+        storage.insert(p)
+    return storage
+
+
 class PathConnectTest1(unittest.TestCase):
 
     def setUp(self):
+        print "PathConnectTest1.setUp()"
         pass
 
     def test_simple_connect(self):
@@ -18,8 +29,9 @@ class PathConnectTest1(unittest.TestCase):
             LineString([[1, 1], [2, 1]])
         ]
 
-        result = Geometry.path_connect(paths)
+        result = Geometry.path_connect(mkstorage(paths))
 
+        result = list(result.get_objects())
         self.assertEqual(len(result), 1)
         self.assertTrue(result[0].equals(LineString([[0, 0], [1, 1], [2, 1]])))
 
@@ -30,8 +42,9 @@ class PathConnectTest1(unittest.TestCase):
             LineString([[-0.5, 0.5], [0.5, 0]])
         ]
 
-        result = Geometry.path_connect(paths)
+        result = Geometry.path_connect(mkstorage(paths))
 
+        result = list(result.get_objects())
         self.assertEqual(len(result), 2)
         matches = [p for p in result if p.equals(LineString([[0, 0], [1, 1], [2, 1]]))]
         self.assertEqual(len(matches), 1)
@@ -46,8 +59,9 @@ class PathConnectTest1(unittest.TestCase):
                 LineString([[1 + offset_x, 1 + offset_y], [2 + offset_x, 1 + offset_y]])
             ]
 
-            result = Geometry.path_connect(paths)
+            result = Geometry.path_connect(mkstorage(paths))
 
+            result = list(result.get_objects())
             self.assertEqual(len(result), 1)
             self.assertTrue(result[0].equals(LineString([[0 + offset_x, 0 + offset_y],
                                                          [1 + offset_x, 1 + offset_y],
@@ -63,8 +77,9 @@ class PathConnectTest1(unittest.TestCase):
             LinearRing([[1, 1], [2, 2], [1, 3], [0, 2]])
         ]
 
-        result = Geometry.path_connect(paths)
+        result = Geometry.path_connect(mkstorage(paths))
 
+        result = list(result.get_objects())
         self.assertEqual(len(result), 2)
         matches = [p for p in result if p.equals(LineString([[0, 0], [1, 1], [2, 1]]))]
         self.assertEqual(len(matches), 1)