From 6733ebbfa80891acd16fc4ab646efbda2cf4805a Mon Sep 17 00:00:00 2001 From: jpcaram Date: Thu, 29 Jan 2015 15:52:03 -0500 Subject: [PATCH] Refactored paint_connect() and path_connect() to receive and return FlatCAMRTreeStorage objects. Updated unittests acordingly. --- FlatCAMApp.py | 6 ++ camlib.py | 148 ++++++++++++++++++++------------------ tests/test_paint.py | 38 ++++++---- tests/test_pathconnect.py | 23 ++++-- 4 files changed, 131 insertions(+), 84 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 0a89fcaf..c7ddc82f 100644 --- a/FlatCAMApp.py +++ b/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 diff --git a/camlib.py b/camlib.py index c0b20fc9..b46a1f7e 100644 --- a/camlib.py +++ b/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): diff --git a/tests/test_paint.py b/tests/test_paint.py index 1101b8a5..c302367b 100644 --- a/tests/test_paint.py +++ b/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 diff --git a/tests/test_pathconnect.py b/tests/test_pathconnect.py index 0bde0528..ec422c1e 100644 --- a/tests/test_pathconnect.py +++ b/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)