From c20c6b0abfb730fbda11bdcfde794dae62d581e7 Mon Sep 17 00:00:00 2001 From: jpcaram Date: Wed, 31 Dec 2014 16:45:10 -0500 Subject: [PATCH 1/5] Using FlatCAMRTreeStorage in DrawingTool. --- .gitignore | 1 + FlatCAMDraw.py | 222 +++++++++++++++++++++++++++++-------------------- camlib.py | 63 +++++++++++++- 3 files changed, 192 insertions(+), 94 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..7e99e367 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc \ No newline at end of file diff --git a/FlatCAMDraw.py b/FlatCAMDraw.py index 9918ed97..6314a6fd 100644 --- a/FlatCAMDraw.py +++ b/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,26 @@ 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 + _, closest_shape = self.storage.nearest(point) - for shape in self.shape_buffer: + if self.draw_app.key != 'control': + self.draw_app.selected = [] - # Remove all if 'control' is not help - if self.draw_app.key != 'control': - #shape["selected"] = False - self.draw_app.set_unselected(shape) + self.draw_app.set_selected(closest_shape) - # TODO: Do this with rtree? - dist = Point(point).distance(cascaded_union(shape.geo)) - if dist < min_distance: - closest_shape = shape - min_distance = dist - - 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." @@ -565,12 +592,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 +603,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 +647,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:] @@ -653,7 +707,9 @@ class FlatCAMDraw(QtCore.QObject): 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): @@ -675,14 +731,13 @@ class FlatCAMDraw(QtCore.QObject): geometry = [fcgeometry.solid_geometry] # Delete contents of editor. - self.shape_buffer = [] + #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)) + #self.shape_buffer.append(DrawToolShape(geometry)) + self.add_shape(DrawToolShape(shape.flatten())) self.replot() self.drawing_toolbar.setDisabled(False) @@ -795,9 +850,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) @@ -841,9 +894,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) @@ -961,49 +1013,24 @@ class FlatCAMDraw(QtCore.QObject): 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 +1103,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 + # 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 - # 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) + self.storage.remove(shape) if shape in self.selected: self.selected.remove(shape) @@ -1105,6 +1135,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 +1173,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 +1208,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): @@ -1185,7 +1224,8 @@ class FlatCAMDraw(QtCore.QObject): # Delete originals. for shape in self.get_selected(): - self.shape_buffer.remove(shape) + #self.shape_buffer.remove(shape) + self.delete_shape(shape) # TODO: This will crash # Selected geometry is now gone! self.selected = [] diff --git a/camlib.py b/camlib.py index 575cfadc..1985cdcc 100644 --- a/camlib.py +++ b/camlib.py @@ -138,6 +138,47 @@ class Geometry(object): else: return self.solid_geometry.bounds + def flatten(self, geometry=None, reset=True): + if geometry is None: + geometry = self.solid_geometry + + if reset: + self.flat_geometry = [] + + ## If iterable, expand recursively. + try: + for geo in geometry: + self.flatten(geometry=geo, reset=False) + + ## Not iterable, do the actual indexing and add. + except TypeError: + if type(geometry) == Polygon: + self.flat_geometry.append(geometry) + + return self.flat_geometry + + def make2Dindex(self): + + 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 @@ -3188,11 +3229,14 @@ def distance(pt1, pt2): class FlatCAMRTree(object): + def __init__(self): self.rti = rtindex.Index() self.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 +3251,18 @@ class FlatCAMRTree(object): self.grow_obj2points(objid) self.obj2points[objid] = [] - for pt in obj.coords: + #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 in range(len(self.obj2points[objid])): + for i, pt in enumerate(self.get_points(obj)): + #pt = obj.coords[i] + #pt = self.get_points(obj)[i] self.rti.delete(self.obj2points[objid][i], (pt[0], pt[1], pt[0], pt[1])) def nearest(self, pt): @@ -3241,9 +3288,19 @@ class FlatCAMRTreeStorage(FlatCAMRTree): 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 From 9d7b480378eb02cf476fcbc5052b8e31a9042edc Mon Sep 17 00:00:00 2001 From: jpcaram Date: Wed, 31 Dec 2014 17:21:52 -0500 Subject: [PATCH 2/5] Recursive handling when plotting shapes and getting points from shapes (for index). --- FlatCAMDraw.py | 94 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 63 insertions(+), 31 deletions(-) diff --git a/FlatCAMDraw.py b/FlatCAMDraw.py index 6314a6fd..409e46b2 100644 --- a/FlatCAMDraw.py +++ b/FlatCAMDraw.py @@ -435,6 +435,7 @@ class FCSelect(DrawTool): self.draw_app.selected = [] self.draw_app.set_selected(closest_shape) + self.draw_app.app.log.debug("Selected shape containing: " + str(closest_shape.geo)) return "" @@ -849,6 +850,8 @@ class FlatCAMDraw(QtCore.QObject): if isinstance(geo, DrawToolShape) and geo.geo is not None: + print geo.geo + # Remove any previous utility shape self.delete_utility_geometry() @@ -858,7 +861,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) @@ -969,46 +975,72 @@ class FlatCAMDraw(QtCore.QObject): if geometry is None: geometry = self.active_tool.geometry + # try: + # _ = iter(geometry) + # iterable_geometry = geometry + # except TypeError: + # iterable_geometry = [geometry] + + ## Iterable: Descend into each element. try: - _ = iter(geometry) - iterable_geometry = geometry + for geo in geometry: + plot_elements += self.plot_shape(geometry=geo, + linespec=linespec, + linewidth=linewidth, + animated=animated) + + ## Non-iterable except TypeError: - iterable_geometry = [geometry] - for geo in iterable_geometry: + ## DrawToolShape + if isinstance(geometry, DrawToolShape): + plot_elements += self.plot_shape(geometry=geometry.geo, + linespec=linespec, + linewidth=linewidth, + animated=animated) - if type(geo) == Polygon: - x, y = geo.exterior.coords.xy + ## 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) - 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 + # continue - if type(geo) == LineString or type(geo) == LinearRing: - x, y = geo.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) == 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 + 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() From 709cb4bff18d56c4dd583aefb5e53fa47840baf1 Mon Sep 17 00:00:00 2001 From: jpcgt Date: Thu, 1 Jan 2015 17:19:01 +0000 Subject: [PATCH 3/5] README.md edited online with Bitbucket --- README.md | 56 +------------------------------------------------------ 1 file changed, 1 insertion(+), 55 deletions(-) diff --git a/README.md b/README.md index 23edf12d..352e587c 100644 --- a/README.md +++ b/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. From 977b5b1f915ce941e7840a27c848a5eec5cd5e89 Mon Sep 17 00:00:00 2001 From: jpcaram Date: Thu, 1 Jan 2015 14:59:45 -0500 Subject: [PATCH 4/5] Using FlatCamRTreeStorage in copper clearing algorithm. --- FlatCAMDraw.py | 27 ++---- camlib.py | 194 +++++++++++++++++++++++----------------- doc/source/planning.rst | 2 +- tests/test_fcrts.py | 37 ++++++++ 4 files changed, 159 insertions(+), 101 deletions(-) create mode 100644 tests/test_fcrts.py diff --git a/FlatCAMDraw.py b/FlatCAMDraw.py index 409e46b2..60eb99be 100644 --- a/FlatCAMDraw.py +++ b/FlatCAMDraw.py @@ -704,7 +704,6 @@ 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 @@ -721,24 +720,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(DrawToolShape(geometry)) - self.add_shape(DrawToolShape(shape.flatten())) + 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) @@ -850,8 +839,6 @@ class FlatCAMDraw(QtCore.QObject): if isinstance(geo, DrawToolShape) and geo.geo is not None: - print geo.geo - # Remove any previous utility shape self.delete_utility_geometry() @@ -1255,9 +1242,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) - self.delete_shape(shape) # TODO: This will crash + 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 = [] diff --git a/camlib.py b/camlib.py index 1985cdcc..416ff517 100644 --- a/camlib.py +++ b/camlib.py @@ -138,7 +138,7 @@ class Geometry(object): else: return self.solid_geometry.bounds - def flatten(self, geometry=None, reset=True): + def flatten(self, geometry=None, reset=True, pathonly=False): if geometry is None: geometry = self.solid_geometry @@ -148,12 +148,21 @@ class Geometry(object): ## If iterable, expand recursively. try: for geo in geometry: - self.flatten(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: + if pathonly and type(geometry) == Polygon: + self.flat_geometry.append(geometry.exterior) + self.flatten(geometry=geometry.interiors, + reset=False, + pathonly=True) + else: self.flat_geometry.append(geometry) + # if type(geometry) == Polygon: + # self.flat_geometry.append(geometry) return self.flat_geometry @@ -178,50 +187,49 @@ class Geometry(object): 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 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): """ @@ -2323,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 @@ -2347,37 +2366,44 @@ 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]] + pt, geo = storage.nearest(current_pt) + try: + while True: + path_count += 1 + print "Current: ", "(%.3f, %.3f)" % current_pt - # 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 + # TODO: There shoudn't be any None in geometry.flatten() + # if geo is None: + # storage.remove(geo) + # continue - # Reverse if end is closest. - if distance2start > distance2stop: - print " Reversing!" - geo.coords = list(geo.coords)[::-1] + # Remove before modifying, otherwise + # deletion will fail. + storage.remove(geo) - # 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)))) + if list(pt) == list(geo.coords[-1]): + print "Reversing" + geo.coords = list(geo.coords)[::-1] - # 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)) + # 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) + + except StopIteration: # Nothing found in storage. + pass log.debug("%s paths traced." % path_count) @@ -3231,8 +3257,15 @@ 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 @@ -3251,7 +3284,6 @@ 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)) @@ -3259,10 +3291,7 @@ class FlatCAMRTree(object): def remove_obj(self, objid, obj): # Use all ptids to delete from index - #for i in range(len(self.obj2points[objid])): for i, pt in enumerate(self.get_points(obj)): - #pt = obj.coords[i] - #pt = self.get_points(obj)[i] self.rti.delete(self.obj2points[objid][i], (pt[0], pt[1], pt[0], pt[1])) def nearest(self, pt): @@ -3280,8 +3309,13 @@ 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): diff --git a/doc/source/planning.rst b/doc/source/planning.rst index 7a3a87f4..4d9ea451 100644 --- a/doc/source/planning.rst +++ b/doc/source/planning.rst @@ -12,7 +12,7 @@ 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. diff --git a/tests/test_fcrts.py b/tests/test_fcrts.py new file mode 100644 index 00000000..7c4afbbb --- /dev/null +++ b/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) From f17f95535546a6816441965a3a723882870ff2b1 Mon Sep 17 00:00:00 2001 From: jpcaram Date: Thu, 1 Jan 2015 15:41:50 -0500 Subject: [PATCH 5/5] Added delete button in drawing toolbar. --- FlatCAMDraw.py | 12 +++++++++--- camlib.py | 9 ++------- doc/source/planning.rst | 13 +++++++++++-- share/deleteshape16.png | Bin 0 -> 542 bytes share/deleteshape24.png | Bin 0 -> 679 bytes share/deleteshape32.png | Bin 0 -> 886 bytes 6 files changed, 22 insertions(+), 12 deletions(-) create mode 100644 share/deleteshape16.png create mode 100644 share/deleteshape24.png create mode 100644 share/deleteshape32.png diff --git a/FlatCAMDraw.py b/FlatCAMDraw.py index 60eb99be..fb5d2b96 100644 --- a/FlatCAMDraw.py +++ b/FlatCAMDraw.py @@ -526,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') @@ -535,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() @@ -569,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 = { @@ -926,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. diff --git a/camlib.py b/camlib.py index 416ff517..8e5319b3 100644 --- a/camlib.py +++ b/camlib.py @@ -2370,19 +2370,14 @@ class CNCjob(Geometry): try: while True: path_count += 1 - print "Current: ", "(%.3f, %.3f)" % current_pt - - # TODO: There shoudn't be any None in geometry.flatten() - # if geo is None: - # storage.remove(geo) - # continue + #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" + #print "Reversing" geo.coords = list(geo.coords)[::-1] # G-code diff --git a/doc/source/planning.rst b/doc/source/planning.rst index 4d9ea451..6dd24bc5 100644 --- a/doc/source/planning.rst +++ b/doc/source/planning.rst @@ -15,12 +15,14 @@ Drawing * [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. \ No newline at end of file +* [DONE] `cascaded_union([])` bug requires more testing. + * Old version of GEOS + + +Other +----- + +* Unit testing \ No newline at end of file diff --git a/share/deleteshape16.png b/share/deleteshape16.png new file mode 100644 index 0000000000000000000000000000000000000000..cb672fa24fe7938f8a26fb8e444dd3a96607c9ad GIT binary patch literal 542 zcmV+(0^$9MP)ugK~y-6rIWF16LA#BKhFy!IgTVWn1dtO49(n45JU$DvCz#$ z2bV}%vI$wTgwSRXL2w8mlC0uj_qw#ArT>7Sb5I=4($EN+(0i(D#+^>*zU#V9KA%4U)>ZXS#6oatwc4R+n&16a-vJ*1&15oP8;!=NS$<>+sZ{E; zR4Tmyz5ow_JHUrZrSd8sk58|hLO3kT`Um_9LC1+iVmyx<5%4_EXt&$XfLp*p&`GP+ z+L%lx5>c2LmE}0j381-L?pwWHZ&j<+*XeZn2+(4&_!EeQb0k=05wU<>;I?hsKg#9u z`(!dX1{e$miEg*MJ{%60fF0nGs*W#p05mv%`8~j#vq;y0&pzkr1s)=-0$cNvXGFGv g<%osX85))JQ$Ph|`Tzg`07*qoM6N<$g1$KE&Hw-a literal 0 HcmV?d00001 diff --git a/share/deleteshape24.png b/share/deleteshape24.png new file mode 100644 index 0000000000000000000000000000000000000000..8b2cdda91473f8310646694ba060a3e6a0c9d74e GIT binary patch literal 679 zcmV;Y0$BZtP)k8FWQhbW?9;ba!ELWdL_~cP?peYja~^ zaAhuUa%Y?FJQ@H10whU9K~zYI?UgZz6HyR`zgahgfTTJwAXrHbmTM$sWC@0d#TJI+ z;I6nrJBvt8gn<2;O`{SE8&R$ZY7q#ULTp4K<)yKa6qXl6NF_w{+_6Zan~l4P$YJAy z*UUff|CxX0y(g;5aW3On+5d@vh@26TlYdGiB4$=Z!xttQ}Nyu|`97pT*`qL<$$a`QIhHh?dl1L<4 z(@+w40oS@vNf#83%!Z1+C|++V0e)kL?U^iVRNfSmYcZR(}i=sThD%Jq)-Q* zoAaD=zF(g6<2|n^r8rD64i)`Bfg?*mL~e;lLPVk>(zM^;B63bd7DQxGL@sHKlu|_p z@B+$mW`U+syJ`pU0SQZ-Rjr-I@idS*2nh|i0-ow@k856JO+>B&4}fENb&td0m>C-z zyWY{!@x9Ws$6~Qg!!YjTa=B)UItkp+rm5T@5|LrxzJ&y>t*wlXj^cDWwLbB9oJb_X z=H{kVodo)oQg3uVWxGHr^#~Xdkpjcj)m5U=C|g@wHG$jh_H-riU7(7mudnZkVHnn2 zDx1yrYG`Q4XPRcaX`1bUKw!`?jL%jZm&>I>q0qf*@qepn(1~h7}eL$?k~XT?FFsI1>{S z=S#qozy;t6CEYPTKJL>Et9=nzTwHWVA`vo~jIH2g7+(T`z-7}kznP}_!t3?ku##9` zUvG>?qik$!II6`j`z?T-oZId8+3oG^6T3Q>%SAXGrnJxJ=jWN8o+g*u%Sdx`^I9sE zI-`{OS~mep!4;mp9C#%Xz)N7T90_f)=)eubNOgC2Uu$V;St