From 6b51f03db213c49bd755564c7ff5389d65d59d1f Mon Sep 17 00:00:00 2001 From: jpcaram Date: Sun, 25 Jan 2015 16:55:22 -0500 Subject: [PATCH] "Paint connect" functional. Added to seed-based painting and unit-tested. "Path connect" still pending. Disabled. "Path connect" unit test added. --- FlatCAMObj.py | 2 + camlib.py | 190 ++++++++++++++++++++---------------- tests/test_paint.py | 197 ++++++++++++++++++++++++++++++++++++++ tests/test_pathconnect.py | 27 ++++++ tests/test_plotg.py | 46 +++++++++ 5 files changed, 381 insertions(+), 81 deletions(-) create mode 100644 tests/test_paint.py create mode 100644 tests/test_pathconnect.py create mode 100644 tests/test_plotg.py diff --git a/FlatCAMObj.py b/FlatCAMObj.py index bb5186b6..4cdd118d 100644 --- a/FlatCAMObj.py +++ b/FlatCAMObj.py @@ -963,6 +963,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): # To be called after clicking on the plot. def doit(event): + self.app.info("Painting polygon...") self.app.plotcanvas.mpl_disconnect(subscription) point = [event.xdata, event.ydata] 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) geo_obj.solid_geometry = cp geo_obj.options["cnctooldia"] = tooldia + self.app.inform.emit("Done.") name = self.options["name"] + "_paint" self.app.new_object("geometry", name, gen_paintarea) diff --git a/camlib.py b/camlib.py index 5971a68e..e666cb2f 100644 --- a/camlib.py +++ b/camlib.py @@ -37,7 +37,7 @@ from descartes.patch import PolygonPatch import simplejson as json # TODO: Commented for FlatCAM packaging with cx_freeze -#from matplotlib.pyplot import plot +from matplotlib.pyplot import plot, subplot import logging @@ -388,12 +388,21 @@ class Geometry(object): inner_edges.append(y) geoms += outer_edges + inner_edges - # Optimization + # Optimization: Join paths # TODO: Re-architecture? + # log.debug("Simplifying paths...") g = Geometry() 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 @@ -418,7 +427,8 @@ class Geometry(object): """ 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 within the paint area. This avoids unnecessary tool lifting. @@ -437,17 +447,20 @@ class Geometry(object): for shape in geolist: 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. optimized_paths = [] - temp_path = None path_count = 0 current_pt = (0, 0) pt, geo = storage.nearest(current_pt) try: while True: path_count += 1 + log.debug("Path %d" % path_count) # Remove before modifying, otherwise # deletion will fail. @@ -461,27 +474,35 @@ class Geometry(object): # Straight line from current_pt to pt. # Is the toolpath inside the geometry? jump = LineString([current_pt, pt]).buffer(tooldia / 2) + if jump.within(boundary): + log.debug("Jump to path #%d is inside. Joining." % path_count) + # 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: + # 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] # Next 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 - def path_connect(self): + return optimized_paths + + @staticmethod + def path_connect(pathlist): """ Simplifies a list of paths by joining those whose ends touch. The list of paths of generated from the geometry.flatten() @@ -491,7 +512,7 @@ class Geometry(object): :return: None """ - flat_geometry = self.flatten(pathonly=True) + # flat_geometry = self.flatten(pathonly=True) ## Index first and last points in paths def get_pts(o): @@ -500,7 +521,7 @@ class Geometry(object): storage = FlatCAMRTreeStorage() 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. storage.insert(shape) @@ -558,7 +579,8 @@ class Geometry(object): except StopIteration: # Nothing found in storage. pass - self.flat_geometry = optimized_geometry + #self.flat_geometry = optimized_geometry + return optimized_geometry def convert_units(self, units): """ @@ -2435,6 +2457,7 @@ class CNCjob(Geometry): :return: None """ assert isinstance(geometry, Geometry) + log.debug("generate_from_geometry_2()") ## Flatten the geometry # Only linear elements (no polygons) remain. @@ -2442,12 +2465,16 @@ class CNCjob(Geometry): log.debug("%d paths" % len(flat_geometry)) ## Index first and last points in paths + # What points to index. def get_pts(o): return [o.coords[0], o.coords[-1]] + # Create the indexed storage. storage = FlatCAMRTreeStorage() storage.get_points = get_pts + # Store the geometry + log.debug("Indexing geometry before generating G-Code...") for shape in flat_geometry: if shape is not None: # TODO: This shouldn't have happened. storage.insert(shape) @@ -2455,7 +2482,7 @@ class CNCjob(Geometry): if tooldia is not None: self.tooldia = tooldia - self.input_geometry_bounds = geometry.bounds() + # self.input_geometry_bounds = geometry.bounds() if not append: self.gcode = "" @@ -2470,6 +2497,7 @@ class CNCjob(Geometry): self.gcode += self.pausecode + "\n" ## Iterate over geometry paths getting the nearest each time. + log.debug("Starting G-Code...") path_count = 0 current_pt = (0, 0) pt, geo = storage.nearest(current_pt) @@ -2997,7 +3025,7 @@ def dict2obj(d): return d -def plotg(geo, solid_poly=False): +def plotg(geo, solid_poly=False, color="black"): try: _ = iter(geo) except: @@ -3015,15 +3043,15 @@ def plotg(geo, solid_poly=False): ax.add_patch(patch) else: x, y = g.exterior.coords.xy - plot(x, y) + plot(x, y, color=color) for ints in g.interiors: x, y = ints.coords.xy - plot(x, y) + plot(x, y, color=color) continue if type(g) == LineString or type(g) == LinearRing: x, y = g.coords.xy - plot(x, y) + plot(x, y, color=color) continue if type(g) == Point: @@ -3033,7 +3061,7 @@ def plotg(geo, solid_poly=False): try: _ = iter(g) - plotg(g) + plotg(g, color=color) except: log.error("Cannot plot: " + str(type(g))) continue @@ -3380,58 +3408,58 @@ class FlatCAMRTreeStorage(FlatCAMRTree): 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)] \ No newline at end of file +# 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)] \ No newline at end of file diff --git a/tests/test_paint.py b/tests/test_paint.py new file mode 100644 index 00000000..1101b8a5 --- /dev/null +++ b/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() \ No newline at end of file diff --git a/tests/test_pathconnect.py b/tests/test_pathconnect.py new file mode 100644 index 00000000..7b3f296c --- /dev/null +++ b/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() \ No newline at end of file diff --git a/tests/test_plotg.py b/tests/test_plotg.py new file mode 100644 index 00000000..364d5622 --- /dev/null +++ b/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() \ No newline at end of file