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..fb5d2b96 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,27 @@ 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) + self.draw_app.app.log.debug("Selected shape containing: " + str(closest_shape.geo)) - # 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." @@ -498,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') @@ -507,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() @@ -541,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 = { @@ -565,12 +595,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 +606,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 +650,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:] @@ -649,11 +706,12 @@ 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 - self.shape_buffer = [] + #self.shape_buffer = [] + self.selected = [] + self.storage = FlatCAMDraw.make_storage() self.replot() def edit_fcgeometry(self, fcgeometry): @@ -664,25 +722,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({'geometry': shape, - # # 'selected': False, - # 'utility': False}) - self.shape_buffer.append(DrawToolShape(geometry)) + 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) @@ -795,9 +842,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) @@ -805,7 +850,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) @@ -841,9 +889,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) @@ -881,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. @@ -917,93 +968,94 @@ 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() - 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 +1128,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 +1160,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 +1198,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 +1233,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): @@ -1184,8 +1248,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) + 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/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. diff --git a/camlib.py b/camlib.py index 575cfadc..8e5319b3 100644 --- a/camlib.py +++ b/camlib.py @@ -138,16 +138,7 @@ class Geometry(object): else: return self.solid_geometry.bounds - 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 - """ - + def flatten(self, geometry=None, reset=True, pathonly=False): if geometry is None: geometry = self.solid_geometry @@ -157,30 +148,88 @@ class Geometry(object): ## If iterable, expand recursively. try: for geo in geometry: - self.flatten_to_paths(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: - 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]) + if pathonly and type(geometry) == Polygon: + self.flat_geometry.append(geometry.exterior) + self.flatten(geometry=geometry.interiors, + reset=False, + pathonly=True) 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]) + self.flat_geometry.append(geometry) + # if type(geometry) == Polygon: + # self.flat_geometry.append(geometry) - return self.flat_geometry, self.flat_geometry_rtree + 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 + # 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): """ @@ -2282,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 @@ -2306,37 +2366,39 @@ 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 + # Remove before modifying, otherwise + # deletion will fail. + storage.remove(geo) - # Reverse if end is closest. - if distance2start > distance2stop: - print " Reversing!" - geo.coords = list(geo.coords)[::-1] + if list(pt) == list(geo.coords[-1]): + #print "Reversing" + geo.coords = list(geo.coords)[::-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)))) + # 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] - hits = list(rti.nearest(current_pt, 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] + + # Next + pt, geo = storage.nearest(current_pt) + + except StopIteration: # Nothing found in storage. + pass log.debug("%s paths traced." % path_count) @@ -3188,11 +3250,21 @@ 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 + def grow_obj2points(self, idx): if len(self.obj2points) > idx: # len == 2, idx == 1, ok. @@ -3207,15 +3279,14 @@ 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)) 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, pt in enumerate(self.get_points(obj)): self.rti.delete(self.obj2points[objid][i], (pt[0], pt[1], pt[0], pt[1])) def nearest(self, pt): @@ -3233,17 +3304,32 @@ 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): 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 diff --git a/doc/source/planning.rst b/doc/source/planning.rst index 7a3a87f4..6dd24bc5 100644 --- a/doc/source/planning.rst +++ b/doc/source/planning.rst @@ -12,15 +12,17 @@ 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. + * 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 00000000..cb672fa2 Binary files /dev/null and b/share/deleteshape16.png differ diff --git a/share/deleteshape24.png b/share/deleteshape24.png new file mode 100644 index 00000000..8b2cdda9 Binary files /dev/null and b/share/deleteshape24.png differ diff --git a/share/deleteshape32.png b/share/deleteshape32.png new file mode 100644 index 00000000..822c423f Binary files /dev/null and b/share/deleteshape32.png differ 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)