From fe2b4c7478d07dfa048722ba930d4bb542103a23 Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Sun, 7 Dec 2014 14:53:33 -0500 Subject: [PATCH] Added Feed Method for clearing polygon. Some minor correction to Geometry.plot() --- FlatCAMCommon.py | 1 + FlatCAMDraw.py | 17 ++++-- FlatCAMObj.py | 144 +++++++++++++++++++++++++++++++++++------------ ObjectUI.py | 47 +++++++++++++++- camlib.py | 136 ++++++++++++++++++++++++++++++++++---------- 5 files changed, 272 insertions(+), 73 deletions(-) diff --git a/FlatCAMCommon.py b/FlatCAMCommon.py index ea431927..6536c8a2 100644 --- a/FlatCAMCommon.py +++ b/FlatCAMCommon.py @@ -37,3 +37,4 @@ class LoudDict(dict): """ self.callback = callback + diff --git a/FlatCAMDraw.py b/FlatCAMDraw.py index 882144be..5ad10ce4 100644 --- a/FlatCAMDraw.py +++ b/FlatCAMDraw.py @@ -383,11 +383,15 @@ class FlatCAMDraw(QtCore.QObject): :param fcgeometry: FlatCAMGeometry :return: None """ - try: - _ = iter(fcgeometry.solid_geometry) - geometry = fcgeometry.solid_geometry - except TypeError: - geometry = [fcgeometry.solid_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 = [] @@ -650,6 +654,9 @@ class FlatCAMDraw(QtCore.QObject): self.app.log.debug("plot_all()") self.axes.cla() for shape in self.shape_buffer: + if shape['geometry'] is None: # TODO: This shouldn't have happened + continue + if shape['utility']: self.plot_shape(geometry=shape['geometry'], linespec='k--', linewidth=1) continue diff --git a/FlatCAMObj.py b/FlatCAMObj.py index 3b0ec999..3f88c178 100644 --- a/FlatCAMObj.py +++ b/FlatCAMObj.py @@ -567,6 +567,7 @@ class FlatCAMExcellon(FlatCAMObj, Excellon): "travelz": 0.1, "feedrate": 5.0, # "toolselection": "" + "tooldia": 0.1 }) # TODO: Document this. @@ -613,6 +614,7 @@ class FlatCAMExcellon(FlatCAMObj, Excellon): "travelz": self.ui.travelz_entry, "feedrate": self.ui.feedrate_entry, # "toolselection": self.ui.tools_entry + "tooldia": self.ui.tooldia_entry }) assert isinstance(self.ui, ExcellonObjectUI) @@ -620,13 +622,54 @@ class FlatCAMExcellon(FlatCAMObj, Excellon): self.ui.solid_cb.stateChanged.connect(self.on_solid_cb_click) # self.ui.choose_tools_button.clicked.connect(self.show_tool_chooser) self.ui.generate_cnc_button.clicked.connect(self.on_create_cncjob_button_click) + self.ui.generate_milling_button.clicked.connect(self.on_generate_milling_button_click) + + def get_selected_tools_list(self): + """ + Returns the keys to the self.tools dictionary corresponding + to the selections on the tool list in the GUI. + """ + return [str(x.text()) for x in self.ui.tools_table.selectedItems()] + + def on_generate_milling_button_click(self, *args): + self.app.report_usage("excellon_on_create_milling_button") + self.read_form() + + # Get the tools from the list + tools = self.get_selected_tools_list() + + if len(tools) == 0: + self.app.inform.emit("Please select one or more tools from the list and try again.") + return + + geo_name = self.options["name"] + "_mill" + + def geo_init(geo_obj, app_obj): + assert isinstance(geo_obj, FlatCAMGeometry) + app_obj.progress.emit(20) + + geo_obj.solid_geometry = [] + + for hole in self.drills: + if hole['tool'] in tools: + geo_obj.solid_geometry.append( + Point(hole['point']).buffer(self.tools[hole['tool']]["C"]/2 - self.options["tooldia"]/2).exterior + ) + + def geo_thread(app_obj): + app_obj.new_object("geometry", geo_name, geo_init) + app_obj.progress.emit(100) + + # Send to worker + # self.app.worker.add_task(job_thread, [self.app]) + self.app.worker_task.emit({'fcn': geo_thread, 'params': [self.app]}) def on_create_cncjob_button_click(self, *args): self.app.report_usage("excellon_on_create_cncjob_button") self.read_form() # Get the tools from the list - tools = [str(x.text()) for x in self.ui.tools_table.selectedItems()] + tools = self.get_selected_tools_list() if len(tools) == 0: self.app.inform.emit("Please select one or more tools from the list and try again.") @@ -890,7 +933,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): "cnctooldia": 0.4 / 25.4, "painttooldia": 0.0625, "paintoverlap": 0.15, - "paintmargin": 0.01 + "paintmargin": 0.01, + "paintmethod": "standard" }) # Attributes to be included in serialization @@ -918,7 +962,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): "cnctooldia": self.ui.cnctooldia_entry, "painttooldia": self.ui.painttooldia_entry, "paintoverlap": self.ui.paintoverlap_entry, - "paintmargin": self.ui.paintmargin_entry + "paintmargin": self.ui.paintmargin_entry, + "paintmethod": self.ui.paintmethod_combo }) self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click) @@ -945,13 +990,18 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): subscription = self.app.plotcanvas.mpl_connect('button_press_event', doit) def paint_poly(self, inside_pt, tooldia, overlap): + + # Which polygon. poly = find_polygon(self.solid_geometry, inside_pt) # Initializes the new geometry object def gen_paintarea(geo_obj, app_obj): assert isinstance(geo_obj, FlatCAMGeometry) #assert isinstance(app_obj, App) - cp = clear_poly(poly.buffer(-self.options["paintmargin"]), tooldia, overlap) + #cp = clear_poly(poly.buffer(-self.options["paintmargin"]), tooldia, overlap) + cp = self.clear_polygon(poly.buffer(-self.options["paintmargin"]), tooldia, overlap=overlap) + if self.options["paintmethod"] == "seed": + cp = self.clear_polygon2(poly.buffer(-self.options["paintmargin"]), tooldia, overlap=overlap) geo_obj.solid_geometry = cp geo_obj.options["cnctooldia"] = tooldia @@ -1067,6 +1117,26 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): return factor + def plot_element(self, element): + try: + for sub_el in element: + self.plot_element(sub_el) + except TypeError: + if type(element) == Polygon: + x, y = element.exterior.coords.xy + self.axes.plot(x, y, 'r-') + for ints in element.interiors: + x, y = ints.coords.xy + self.axes.plot(x, y, 'r-') + return + + if type(element) == LineString or type(element) == LinearRing: + x, y = element.coords.xy + self.axes.plot(x, y, 'r-') + return + + FlatCAMApp.App.log.warning("Did not plot:", str(type(element))) + def plot(self): """ Plots the object into its axes. If None, of if the axes @@ -1082,39 +1152,41 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): # Make sure solid_geometry is iterable. # TODO: This method should not modify the object !!! - try: - _ = iter(self.solid_geometry) - except TypeError: - if self.solid_geometry is None: - self.solid_geometry = [] - else: - self.solid_geometry = [self.solid_geometry] + # try: + # _ = iter(self.solid_geometry) + # except TypeError: + # if self.solid_geometry is None: + # self.solid_geometry = [] + # else: + # self.solid_geometry = [self.solid_geometry] + # + # for geo in self.solid_geometry: + # + # if type(geo) == Polygon: + # x, y = geo.exterior.coords.xy + # self.axes.plot(x, y, 'r-') + # for ints in geo.interiors: + # x, y = ints.coords.xy + # self.axes.plot(x, y, 'r-') + # continue + # + # if type(geo) == LineString or type(geo) == LinearRing: + # x, y = geo.coords.xy + # self.axes.plot(x, y, 'r-') + # continue + # + # if type(geo) == MultiPolygon: + # for poly in geo: + # x, y = poly.exterior.coords.xy + # self.axes.plot(x, y, 'r-') + # for ints in poly.interiors: + # x, y = ints.coords.xy + # self.axes.plot(x, y, 'r-') + # continue + # + # FlatCAMApp.App.log.warning("Did not plot:", str(type(geo))) - for geo in self.solid_geometry: - - if type(geo) == Polygon: - x, y = geo.exterior.coords.xy - self.axes.plot(x, y, 'r-') - for ints in geo.interiors: - x, y = ints.coords.xy - self.axes.plot(x, y, 'r-') - continue - - if type(geo) == LineString or type(geo) == LinearRing: - x, y = geo.coords.xy - self.axes.plot(x, y, 'r-') - continue - - if type(geo) == MultiPolygon: - for poly in geo: - x, y = poly.exterior.coords.xy - self.axes.plot(x, y, 'r-') - for ints in poly.interiors: - x, y = ints.coords.xy - self.axes.plot(x, y, 'r-') - continue - - FlatCAMApp.App.log.warning("Did not plot:", str(type(geo))) + self.plot_element(self.solid_geometry) self.app.plotcanvas.auto_adjust_axes() # GLib.idle_add(self.app.plotcanvas.auto_adjust_axes) diff --git a/ObjectUI.py b/ObjectUI.py index 40866863..23a61b46 100644 --- a/ObjectUI.py +++ b/ObjectUI.py @@ -245,7 +245,9 @@ class GeometryObjectUI(ObjectUI): ) self.custom_box.addWidget(self.generate_cnc_button) - ## Paint area + ################ + ## Paint area ## + ################ self.paint_label = QtGui.QLabel('Paint Area:') self.paint_label.setToolTip( "Creates tool paths to cover the\n" @@ -288,7 +290,19 @@ class GeometryObjectUI(ObjectUI): ) grid2.addWidget(marginlabel, 2, 0) self.paintmargin_entry = LengthEntry() - grid2.addWidget(self.paintmargin_entry) + grid2.addWidget(self.paintmargin_entry, 2, 1) + + # Method + methodlabel = QtGui.QLabel('Method:') + methodlabel.setToolTip( + "Algorithm to paint the polygon." + ) + grid2.addWidget(methodlabel, 3, 0) + self.paintmethod_combo = RadioSet([ + {"label": "Standard", "value": "standard"}, + {"label": "Seed-based", "value": "seed"} + ]) + grid2.addWidget(self.paintmethod_combo, 3, 1) # GO Button self.generate_paint_button = QtGui.QPushButton('Generate') @@ -386,6 +400,35 @@ class ExcellonObjectUI(ObjectUI): ) self.custom_box.addWidget(self.generate_cnc_button) + ## Milling Holes + self.mill_hole_label = QtGui.QLabel('Mill Holes') + self.mill_hole_label.setToolTip( + "Create Geometry for milling holes." + ) + self.custom_box.addWidget(self.mill_hole_label) + + grid1 = QtGui.QGridLayout() + self.custom_box.addLayout(grid1) + tdlabel = QtGui.QLabel('Tool dia:') + tdlabel.setToolTip( + "Diameter of the cutting tool." + ) + grid1.addWidget(tdlabel, 0, 0) + self.tooldia_entry = LengthEntry() + grid1.addWidget(self.tooldia_entry, 0, 1) + + choose_tools_label2 = QtGui.QLabel( + "Select from the tools section above\n" + "the tools you want to include." + ) + self.custom_box.addWidget(choose_tools_label2) + + self.generate_milling_button = QtGui.QPushButton('Generate Geometry') + self.generate_milling_button.setToolTip( + "Create the Geometry Object\n" + "for milling toolpaths." + ) + self.custom_box.addWidget(self.generate_milling_button) class GerberObjectUI(ObjectUI): """ diff --git a/camlib.py b/camlib.py index 31f57696..f329d11a 100644 --- a/camlib.py +++ b/camlib.py @@ -216,6 +216,14 @@ class Geometry(object): """ Creates geometry inside a polygon for a tool to cover the whole area. + + This algorithm shrinks the edges of the polygon and takes + the resulting edges as toolpaths. + + :param polygon: Polygon to clear. + :param tooldia: Diameter of the tool. + :param overlap: Overlap of toolpasses. + :return: """ poly_cuts = [polygon.buffer(-tooldia/2.0)] while True: @@ -226,6 +234,58 @@ class Geometry(object): break return poly_cuts + def clear_polygon2(self, polygon, tooldia, seedpoint=None, overlap=0.15): + """ + Creates geometry inside a polygon for a tool to cover + the whole area. + + This algorithm starts with a seed point inside the polygon + and draws circles around it. Arcs inside the polygons are + valid cuts. Finalizes by cutting around the inside edge of + the polygon. + + :param polygon: + :param tooldia: + :param seedpoint: + :param overlap: + :return: + """ + + if seedpoint is None: + seedpoint = polygon.representative_point() + + # Current buffer radius + radius = tooldia/2*(1-overlap) + + # The toolpaths + geoms = [Point(seedpoint).buffer(radius).exterior] + + # Path margin + path_margin = polygon.buffer(-tooldia/2) + + # Grow from seed until outside the box. + while 1: + path = Point(seedpoint).buffer(radius).exterior + path = path.intersection(path_margin) + + # Touches polygon? + if path.is_empty: + break + else: + geoms.append(path) + + radius += tooldia*(1-overlap) + + # Clean edges + 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 + + return geoms + def scale(self, factor): """ Scales all of the object's geometry by a given factor. Override @@ -2695,30 +2755,30 @@ def arc_angle(start, stop, direction): return angle -def clear_poly(poly, tooldia, overlap=0.1): - """ - Creates a list of Shapely geometry objects covering the inside - of a Shapely.Polygon. Use for removing all the copper in a region - or bed flattening. - - :param poly: Target polygon - :type poly: Shapely.Polygon - :param tooldia: Diameter of the tool - :type tooldia: float - :param overlap: Fraction of the tool diameter to overlap - in each pass. - :type overlap: float - :return: list of Shapely.Polygon - :rtype: list - """ - poly_cuts = [poly.buffer(-tooldia/2.0)] - while True: - poly = poly_cuts[-1].buffer(-tooldia*(1-overlap)) - if poly.area > 0: - poly_cuts.append(poly) - else: - break - return poly_cuts +# def clear_poly(poly, tooldia, overlap=0.1): +# """ +# Creates a list of Shapely geometry objects covering the inside +# of a Shapely.Polygon. Use for removing all the copper in a region +# or bed flattening. +# +# :param poly: Target polygon +# :type poly: Shapely.Polygon +# :param tooldia: Diameter of the tool +# :type tooldia: float +# :param overlap: Fraction of the tool diameter to overlap +# in each pass. +# :type overlap: float +# :return: list of Shapely.Polygon +# :rtype: list +# """ +# poly_cuts = [poly.buffer(-tooldia/2.0)] +# while True: +# poly = poly_cuts[-1].buffer(-tooldia*(1-overlap)) +# if poly.area > 0: +# poly_cuts.append(poly) +# else: +# break +# return poly_cuts def find_polygon(poly_set, point): @@ -2775,7 +2835,7 @@ def dict2obj(d): return d -def plotg(geo): +def plotg(geo, solid_poly=False): try: _ = iter(geo) except: @@ -2783,12 +2843,21 @@ def plotg(geo): for g in geo: if type(g) == Polygon: - x, y = g.exterior.coords.xy - plot(x, y) - for ints in g.interiors: - x, y = ints.coords.xy + if solid_poly: + patch = PolygonPatch(g, + facecolor="#BBF268", + edgecolor="#006E20", + alpha=0.75, + zorder=2) + ax = subplot(111) + ax.add_patch(patch) + else: + x, y = g.exterior.coords.xy plot(x, y) - continue + for ints in g.interiors: + x, y = ints.coords.xy + plot(x, y) + continue if type(g) == LineString or type(g) == LinearRing: x, y = g.coords.xy @@ -3025,3 +3094,10 @@ class Zprofile: return [{"path": path.intersection(self.polygons[i]), "z": self.data[i][2]} for i in crossing] + +def autolist(obj): + try: + _ = iter(obj) + return obj + except TypeError: + return [obj] \ No newline at end of file