diff --git a/CHANGELOG.md b/CHANGELOG.md index abe30c2b..8ce067a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ CHANGELOG for FlatCAM beta ================================================= +16.07.2020 + +- added a new method for GCode generation for Geometry objects +- added multiple algorithms for path optimization when generating GCode from an Geometry object beside the original Rtree algorithm: TSA, OR-Tools Basic, OR-Tools metaheuristics +- added controls for Geometry object path optimization in Preferences + 15.07.2020 - added icons to some of the push buttons diff --git a/appGUI/preferences/PreferencesUIManager.py b/appGUI/preferences/PreferencesUIManager.py index 497404a2..33c760e3 100644 --- a/appGUI/preferences/PreferencesUIManager.py +++ b/appGUI/preferences/PreferencesUIManager.py @@ -246,6 +246,8 @@ class PreferencesUIManager: "geometry_cnctooldia": self.ui.geometry_defaults_form.geometry_gen_group.cnctooldia_entry, "geometry_merge_fuse_tools": self.ui.geometry_defaults_form.geometry_gen_group.fuse_tools_cb, "geometry_plot_line": self.ui.geometry_defaults_form.geometry_gen_group.line_color_entry, + "geometry_optimization_type": self.ui.geometry_defaults_form.geometry_gen_group.opt_algorithm_radio, + "geometry_search_time": self.ui.geometry_defaults_form.geometry_gen_group.optimization_time_entry, # Geometry Options "geometry_cutz": self.ui.geometry_defaults_form.geometry_opt_group.cutz_entry, diff --git a/appGUI/preferences/excellon/ExcellonGenPrefGroupUI.py b/appGUI/preferences/excellon/ExcellonGenPrefGroupUI.py index 7be28cef..19ed9452 100644 --- a/appGUI/preferences/excellon/ExcellonGenPrefGroupUI.py +++ b/appGUI/preferences/excellon/ExcellonGenPrefGroupUI.py @@ -207,7 +207,7 @@ class ExcellonGenPrefGroupUI(OptionsGroupUI): separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) grid2.addWidget(separator_line, 7, 0, 1, 2) - self.excellon_general_label = QtWidgets.QLabel("%s:" % _("Excellon Optimization")) + self.excellon_general_label = QtWidgets.QLabel("%s:" % _("Path Optimization")) grid2.addWidget(self.excellon_general_label, 8, 0, 1, 2) self.excellon_optimization_label = QtWidgets.QLabel(_('Algorithm:')) diff --git a/appGUI/preferences/geometry/GeometryGenPrefGroupUI.py b/appGUI/preferences/geometry/GeometryGenPrefGroupUI.py index fb719908..bd264d9f 100644 --- a/appGUI/preferences/geometry/GeometryGenPrefGroupUI.py +++ b/appGUI/preferences/geometry/GeometryGenPrefGroupUI.py @@ -1,7 +1,7 @@ from PyQt5 import QtWidgets from PyQt5.QtCore import QSettings -from appGUI.GUIElements import FCCheckBox, FCSpinner, FCEntry, FCColorEntry +from appGUI.GUIElements import FCCheckBox, FCSpinner, FCEntry, FCColorEntry, RadioSet from appGUI.preferences.OptionsGroupUI import OptionsGroupUI import gettext @@ -86,25 +86,72 @@ class GeometryGenPrefGroupUI(OptionsGroupUI): separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) grid0.addWidget(separator_line, 9, 0, 1, 2) + self.opt_label = QtWidgets.QLabel("%s:" % _("Path Optimization")) + grid0.addWidget(self.opt_label, 10, 0, 1, 2) + + self.opt_algorithm_label = QtWidgets.QLabel(_('Algorithm:')) + self.opt_algorithm_label.setToolTip( + _("This sets the path optimization algorithm.\n" + "- Rtre -> Rtree algorithm\n" + "- MetaHeuristic -> Google OR-Tools algorithm with\n" + "MetaHeuristic Guided Local Path is used. Default search time is 3sec.\n" + "- Basic -> Using Google OR-Tools Basic algorithm\n" + "- TSA -> Using Travelling Salesman algorithm\n" + "\n" + "If this control is disabled, then FlatCAM works in 32bit mode and it uses\n" + "Travelling Salesman algorithm for path optimization.") + ) + + self.opt_algorithm_radio = RadioSet( + [ + {'label': _('Rtree'), 'value': 'R'}, + {'label': _('MetaHeuristic'), 'value': 'M'}, + {'label': _('Basic'), 'value': 'B'}, + {'label': _('TSA'), 'value': 'T'} + ], orientation='vertical', stretch=False) + + grid0.addWidget(self.opt_algorithm_label, 12, 0) + grid0.addWidget(self.opt_algorithm_radio, 12, 1) + + self.optimization_time_label = QtWidgets.QLabel('%s:' % _('Duration')) + self.optimization_time_label.setToolTip( + _("When OR-Tools Metaheuristic (MH) is enabled there is a\n" + "maximum threshold for how much time is spent doing the\n" + "path optimization. This max duration is set here.\n" + "In seconds.") + + ) + + self.optimization_time_entry = FCSpinner() + self.optimization_time_entry.set_range(0, 999) + + grid0.addWidget(self.optimization_time_label, 14, 0) + grid0.addWidget(self.optimization_time_entry, 14, 1) + + separator_line = QtWidgets.QFrame() + separator_line.setFrameShape(QtWidgets.QFrame.HLine) + separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) + grid0.addWidget(separator_line, 16, 0, 1, 2) + # Fuse Tools self.join_geo_label = QtWidgets.QLabel('%s:' % _('Join Option')) - grid0.addWidget(self.join_geo_label, 10, 0, 1, 2) + grid0.addWidget(self.join_geo_label, 18, 0, 1, 2) self.fuse_tools_cb = FCCheckBox(_("Fuse Tools")) self.fuse_tools_cb.setToolTip( _("When checked the joined (merged) object tools\n" "will be merged also but only if they share some of their attributes.") ) - grid0.addWidget(self.fuse_tools_cb, 11, 0, 1, 2) + grid0.addWidget(self.fuse_tools_cb, 20, 0, 1, 2) separator_line = QtWidgets.QFrame() separator_line.setFrameShape(QtWidgets.QFrame.HLine) separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) - grid0.addWidget(separator_line, 12, 0, 1, 2) + grid0.addWidget(separator_line, 22, 0, 1, 2) # Geometry Object Color self.gerber_color_label = QtWidgets.QLabel('%s:' % _('Object Color')) - grid0.addWidget(self.gerber_color_label, 13, 0, 1, 2) + grid0.addWidget(self.gerber_color_label, 24, 0, 1, 2) # Plot Line Color self.line_color_label = QtWidgets.QLabel('%s:' % _('Outline')) @@ -113,8 +160,8 @@ class GeometryGenPrefGroupUI(OptionsGroupUI): ) self.line_color_entry = FCColorEntry() - grid0.addWidget(self.line_color_label, 14, 0) - grid0.addWidget(self.line_color_entry, 14, 1) + grid0.addWidget(self.line_color_label, 26, 0) + grid0.addWidget(self.line_color_entry, 26, 1) self.layout.addStretch() diff --git a/appObjects/FlatCAMGeometry.py b/appObjects/FlatCAMGeometry.py index eb4ca7ac..76395ebd 100644 --- a/appObjects/FlatCAMGeometry.py +++ b/appObjects/FlatCAMGeometry.py @@ -474,41 +474,19 @@ class GeometryObject(FlatCAMObj, Geometry): # store here the default data for Geometry Data self.default_data = {} - self.default_data.update({ - "name": None, - "plot": None, - "cutz": None, - "vtipdia": None, - "vtipangle": None, - "travelz": None, - "feedrate": None, - "feedrate_z": None, - "feedrate_rapid": None, - "dwell": None, - "dwelltime": None, - "multidepth": None, - "ppname_g": None, - "depthperpass": None, - "extracut": None, - "extracut_length": None, - "toolchange": None, - "toolchangez": None, - "endz": None, - "endxy": '', - "area_exclusion": None, - "area_shape": None, - "area_strategy": None, - "area_overz": None, - "spindlespeed": 0, - "toolchangexy": None, - "startz": None - }) + for opt_key, opt_val in self.app.options.items(): + if opt_key.find('geometry' + "_") == 0: + oname = opt_key[len('geometry') + 1:] + self.default_data[oname] = self.app.options[opt_key] + if opt_key.find('tools_mill' + "_") == 0: + oname = opt_key[len('tools_mill') + 1:] + self.default_data[oname] = self.app.options[opt_key] # fill in self.default_data values from self.options - for def_key in self.default_data: - for opt_key, opt_val in self.options.items(): - if def_key == opt_key: - self.default_data[def_key] = deepcopy(opt_val) + # for def_key in self.default_data: + # for opt_key, opt_val in self.options.items(): + # if def_key == opt_key: + # self.default_data[def_key] = deepcopy(opt_val) if type(self.options["cnctooldia"]) == float: tools_list = [self.options["cnctooldia"]] @@ -1809,16 +1787,6 @@ class GeometryObject(FlatCAMObj, Geometry): # test to see if we have tools available in the tool table if self.ui.geo_tools_table.selectedItems(): for x in self.ui.geo_tools_table.selectedItems(): - # try: - # tooldia = float(self.ui.geo_tools_table.item(x.row(), 1).text()) - # except ValueError: - # # try to convert comma to decimal point. if it's still not working error message and return - # try: - # tooldia = float(self.ui.geo_tools_table.item(x.row(), 1).text().replace(',', '.')) - # except ValueError: - # self.app.inform.emit('[ERROR_NOTCL] %s' % - # _("Wrong value format entered, use a number.")) - # return tooluid = int(self.ui.geo_tools_table.item(x.row(), 5).text()) for tooluid_key, tooluid_value in self.tools.items(): @@ -1884,6 +1852,7 @@ class GeometryObject(FlatCAMObj, Geometry): self.app.inform.emit(msg) return + self.multigeo = True # Object initialization function for app.app_obj.new_object() # RUNNING ON SEPARATE THREAD! def job_init_single_geometry(job_obj, app_obj): @@ -2134,17 +2103,21 @@ class GeometryObject(FlatCAMObj, Geometry): # it seems that the tolerance needs to be a lot lower value than 0.01 and it was hardcoded initially # to a value of 0.0005 which is 20 times less than 0.01 tol = float(self.app.defaults['global_tolerance']) / 20 - res = job_obj.generate_from_multitool_geometry( - tool_solid_geometry, tooldia=tooldia_val, offset=tool_offset, - tolerance=tol, z_cut=z_cut, z_move=z_move, - feedrate=feedrate, feedrate_z=feedrate_z, feedrate_rapid=feedrate_rapid, - spindlespeed=spindlespeed, spindledir=spindledir, dwell=dwell, dwelltime=dwelltime, - multidepth=multidepth, depthpercut=depthpercut, - extracut=extracut, extracut_length=extracut_length, startz=startz, endz=endz, endxy=endxy, - toolchange=toolchange, toolchangez=toolchangez, toolchangexy=toolchangexy, - pp_geometry_name=pp_geometry_name, - tool_no=tool_cnt) - + # res = job_obj.generate_from_multitool_geometry( + # tool_solid_geometry, tooldia=tooldia_val, offset=tool_offset, + # tolerance=tol, z_cut=z_cut, z_move=z_move, + # feedrate=feedrate, feedrate_z=feedrate_z, feedrate_rapid=feedrate_rapid, + # spindlespeed=spindlespeed, spindledir=spindledir, dwell=dwell, dwelltime=dwelltime, + # multidepth=multidepth, depthpercut=depthpercut, + # extracut=extracut, extracut_length=extracut_length, startz=startz, endz=endz, endxy=endxy, + # toolchange=toolchange, toolchangez=toolchangez, toolchangexy=toolchangexy, + # pp_geometry_name=pp_geometry_name, + # tool_no=tool_cnt) + tool_lst = list(tools_dict.keys()) + is_first = True if tooluid_key == tool_lst[0] else False + is_last = True if tooluid_key == tool_lst[-1] else False + res = job_obj.geometry_tool_gcode_gen(tooluid_key, tools_dict, first_pt=(0, 0), tolerance = tol, + is_first=is_first, is_last=is_last, toolchange = True) if res == 'fail': log.debug("GeometryObject.mtool_gen_cncjob() --> generate_from_geometry2() failed") return 'fail' diff --git a/appTools/ToolIsolation.py b/appTools/ToolIsolation.py index 591f07bf..b49c00ea 100644 --- a/appTools/ToolIsolation.py +++ b/appTools/ToolIsolation.py @@ -345,8 +345,7 @@ class ToolIsolation(AppTool, Gerber): "feedrate": self.app.defaults["geometry_feedrate"], "feedrate_z": self.app.defaults["geometry_feedrate_z"], "feedrate_rapid": self.app.defaults["geometry_feedrate_rapid"], - "dwell": self.app.defaults["geometry_dwell"], - "dwelltime": self.app.defaults["geometry_dwelltime"], + "multidepth": self.app.defaults["geometry_multidepth"], "ppname_g": self.app.defaults["geometry_ppname_g"], "depthperpass": self.app.defaults["geometry_depthperpass"], @@ -357,7 +356,13 @@ class ToolIsolation(AppTool, Gerber): "endz": self.app.defaults["geometry_endz"], "endxy": self.app.defaults["geometry_endxy"], + "dwell": self.app.defaults["geometry_dwell"], + "dwelltime": self.app.defaults["geometry_dwelltime"], "spindlespeed": self.app.defaults["geometry_spindlespeed"], + "spindledir": self.app.defaults["geometry_spindledir"], + + "optimization_type": self.app.defaults["geometry_optimization_type"], + "search_time": self.app.defaults["geometry_search_time"], "toolchangexy": self.app.defaults["geometry_toolchangexy"], "startz": self.app.defaults["geometry_startz"], diff --git a/camlib.py b/camlib.py index 10164b6b..6f0f0f60 100644 --- a/camlib.py +++ b/camlib.py @@ -2518,8 +2518,11 @@ class CNCjob(Geometry): self.z_end = endz self.xy_end = endxy + self.extracut = False self.extracut_length = None + self.tolerance = self.drawing_tolerance + # used by the self.generate_from_excellon_by_tool() method # but set directly before the actual usage of the method with obj.excellon_optimization_type = value self.excellon_optimization_type = 'No' @@ -2721,7 +2724,7 @@ class CNCjob(Geometry): # Create the data. return [(pt.coords.xy[0][0], pt.coords.xy[1][0]) for pt in points] - def optimized_ortools_meta(self, locations, start=None): + def optimized_ortools_meta(self, locations, start=None, opt_time=0): optimized_path = [] tsp_size = len(locations) @@ -2731,56 +2734,57 @@ class CNCjob(Geometry): depot = 0 if start is None else start # Create routing model. - if tsp_size > 0: - manager = pywrapcp.RoutingIndexManager(tsp_size, num_routes, depot) - routing = pywrapcp.RoutingModel(manager) - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.local_search_metaheuristic = ( - routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH) - - # Set search time limit in milliseconds. - if float(self.app.defaults["excellon_search_time"]) != 0: - search_parameters.time_limit.seconds = int( - float(self.app.defaults["excellon_search_time"])) - else: - search_parameters.time_limit.seconds = 3 - - # Callback to the distance function. The callback takes two - # arguments (the from and to node indices) and returns the distance between them. - dist_between_locations = self.CreateDistanceCallback(locs=locations, manager=manager) - - # if there are no distances then go to the next tool - if not dist_between_locations: - return - - dist_callback = dist_between_locations.Distance - transit_callback_index = routing.RegisterTransitCallback(dist_callback) - routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) - - # Solve, returns a solution if any. - assignment = routing.SolveWithParameters(search_parameters) - - if assignment: - # Solution cost. - log.info("OR-tools metaheuristics - Total distance: " + str(assignment.ObjectiveValue())) - - # Inspect solution. - # Only one route here; otherwise iterate from 0 to routing.vehicles() - 1. - route_number = 0 - node = routing.Start(route_number) - start_node = node - - while not routing.IsEnd(node): - if self.app.abort_flag: - # graceful abort requested by the user - raise grace - - optimized_path.append(node) - node = assignment.Value(routing.NextVar(node)) - else: - log.warning('OR-tools metaheuristics - No solution found.') - else: + if tsp_size == 0: log.warning('OR-tools metaheuristics - Specify an instance greater than 0.') + return optimized_path + + manager = pywrapcp.RoutingIndexManager(tsp_size, num_routes, depot) + routing = pywrapcp.RoutingModel(manager) + search_parameters = pywrapcp.DefaultRoutingSearchParameters() + search_parameters.local_search_metaheuristic = ( + routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH) + + # Set search time limit in milliseconds. + if float(opt_time) != 0: + search_parameters.time_limit.seconds = int( + float(opt_time)) + else: + search_parameters.time_limit.seconds = 3 + + # Callback to the distance function. The callback takes two + # arguments (the from and to node indices) and returns the distance between them. + dist_between_locations = self.CreateDistanceCallback(locs=locations, manager=manager) + + # if there are no distances then go to the next tool + if not dist_between_locations: + return + + dist_callback = dist_between_locations.Distance + transit_callback_index = routing.RegisterTransitCallback(dist_callback) + routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) + + # Solve, returns a solution if any. + assignment = routing.SolveWithParameters(search_parameters) + + if assignment: + # Solution cost. + log.info("OR-tools metaheuristics - Total distance: " + str(assignment.ObjectiveValue())) + + # Inspect solution. + # Only one route here; otherwise iterate from 0 to routing.vehicles() - 1. + route_number = 0 + node = routing.Start(route_number) + start_node = node + + while not routing.IsEnd(node): + if self.app.abort_flag: + # graceful abort requested by the user + raise grace + + optimized_path.append(node) + node = assignment.Value(routing.NextVar(node)) + else: + log.warning('OR-tools metaheuristics - No solution found.') return optimized_path # ############################################# ## @@ -2795,43 +2799,44 @@ class CNCjob(Geometry): depot = 0 if start is None else start # Create routing model. - if tsp_size > 0: - manager = pywrapcp.RoutingIndexManager(tsp_size, num_routes, depot) - routing = pywrapcp.RoutingModel(manager) - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - - # Callback to the distance function. The callback takes two - # arguments (the from and to node indices) and returns the distance between them. - dist_between_locations = self.CreateDistanceCallback(locs=locations, manager=manager) - - # if there are no distances then go to the next tool - if not dist_between_locations: - return - - dist_callback = dist_between_locations.Distance - transit_callback_index = routing.RegisterTransitCallback(dist_callback) - routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) - - # Solve, returns a solution if any. - assignment = routing.SolveWithParameters(search_parameters) - - if assignment: - # Solution cost. - log.info("Total distance: " + str(assignment.ObjectiveValue())) - - # Inspect solution. - # Only one route here; otherwise iterate from 0 to routing.vehicles() - 1. - route_number = 0 - node = routing.Start(route_number) - start_node = node - - while not routing.IsEnd(node): - optimized_path.append(node) - node = assignment.Value(routing.NextVar(node)) - else: - log.warning('No solution found.') - else: + if tsp_size == 0: log.warning('Specify an instance greater than 0.') + return optimized_path + + manager = pywrapcp.RoutingIndexManager(tsp_size, num_routes, depot) + routing = pywrapcp.RoutingModel(manager) + search_parameters = pywrapcp.DefaultRoutingSearchParameters() + + # Callback to the distance function. The callback takes two + # arguments (the from and to node indices) and returns the distance between them. + dist_between_locations = self.CreateDistanceCallback(locs=locations, manager=manager) + + # if there are no distances then go to the next tool + if not dist_between_locations: + return + + dist_callback = dist_between_locations.Distance + transit_callback_index = routing.RegisterTransitCallback(dist_callback) + routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) + + # Solve, returns a solution if any. + assignment = routing.SolveWithParameters(search_parameters) + + if assignment: + # Solution cost. + log.info("Total distance: " + str(assignment.ObjectiveValue())) + + # Inspect solution. + # Only one route here; otherwise iterate from 0 to routing.vehicles() - 1. + route_number = 0 + node = routing.Start(route_number) + start_node = node + + while not routing.IsEnd(node): + optimized_path.append(node) + node = assignment.Value(routing.NextVar(node)) + else: + log.warning('No solution found.') return optimized_path # ############################################# ## @@ -2871,6 +2876,46 @@ class CNCjob(Geometry): must_visit.remove(nearest) return path + def geo_optimized_rtree(self, geometry): + locations = [] + + # ## 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...") + self.app.inform.emit(_("Indexing geometry before generating G-Code...")) + + for geo_shape in geometry: + if self.app.abort_flag: + # graceful abort requested by the user + raise grace + + if geo_shape is not None: + storage.insert(geo_shape) + + current_pt = (0, 0) + pt, geo = storage.nearest(current_pt) + try: + while True: + storage.remove(geo) + locations.append((pt, geo)) + current_pt = geo.coords[-1] + pt, geo = storage.nearest(current_pt) + except StopIteration: + pass + + # if there are no locations then go to the next tool + if not locations: + return 'fail' + + return locations + def check_zcut(self, zcut): if zcut > 0: self.app.inform.emit('[WARNING] %s' % @@ -2980,12 +3025,10 @@ class CNCjob(Geometry): # and now, xy_toolchange is made into a list of floats in format [x, y] if self.xy_toolchange: - self.xy_toolchange = [ - float(eval(a)) for a in self.xy_toolchange.split(",") - ] + self.xy_toolchange = [float(eval(a)) for a in self.xy_toolchange.split(",")] if self.xy_toolchange and len(self.xy_toolchange) != 2: - self.app.inform.emit('[ERROR]%s' % _("The Toolchange X,Y format has to be (x, y).")) + self.app.inform.emit('[ERROR] %s' % _("The Toolchange X,Y format has to be (x, y).")) return 'fail' except Exception as e: log.debug("camlib.CNCJob.generate_from_excellon_by_tool() xy_toolchange --> %s" % str(e)) @@ -3032,7 +3075,8 @@ class CNCjob(Geometry): # if there are no locations then go to the next tool if not locations: return 'fail' - optimized_path = self.optimized_ortools_meta(locations=locations) + opt_time = self.app.defaults["excellon_search_time"] + optimized_path = self.optimized_ortools_meta(locations=locations, opt_time=opt_time) elif opt_type == 'B': locations = self.create_tool_data_array(points=points) # if there are no locations then go to the next tool @@ -3547,7 +3591,8 @@ class CNCjob(Geometry): # if there are no locations then go to the next tool if not locations: continue - optimized_path = self.optimized_ortools_meta(locations=locations) + opt_time = self.app.defaults["excellon_search_time"] + optimized_path = self.optimized_ortools_meta(locations=locations, opt_time=opt_time) elif used_excellon_optimization_type == 'B': if tool in points: locations = self.create_tool_data_array(points=points[tool]) @@ -3782,7 +3827,8 @@ class CNCjob(Geometry): # if there are no locations then go to the next tool if not locations: return 'fail' - optimized_path = self.optimized_ortools_meta(locations=locations) + opt_time = self.app.defaults["excellon_search_time"] + optimized_path = self.optimized_ortools_meta(locations=locations, opt_time=opt_time) elif used_excellon_optimization_type == 'B': if all_points: locations = self.create_tool_data_array(points=all_points) @@ -4931,6 +4977,365 @@ class CNCjob(Geometry): ) return self.gcode + def geometry_tool_gcode_gen(self, tool, tools, first_pt, tolerance, is_first=False, is_last=False, + toolchange=False): + """ + Algorithm to generate GCode from multitool Geometry. + + :param tool: tool number for which to generate GCode + :type tool: int + :param tools: a dictionary holding all the tools and data + :type tools: dict + :param first_pt: a tuple of coordinates for the first point of the current tool + :type first_pt: tuple + :param tolerance: geometry tolerance + :type tolerance: + :param is_first: if the current tool is the first tool (for this we need to add start GCode) + :type is_first: bool + :param is_last: if the current tool is the last tool (for this we need to add the end GCode) + :type is_last: bool + :param toolchange: add toolchange event + :type toolchange: bool + :return: GCode + :rtype: str + """ + + log.debug("Generate_from_multitool_geometry()") + + t_gcode = '' + temp_solid_geometry = [] + + # The Geometry from which we create GCode + geometry = tools[tool]['solid_geometry'] + # ## Flatten the geometry. Only linear elements (no polygons) remain. + flat_geometry = self.flatten(geometry, pathonly=True) + log.debug("%d paths" % len(flat_geometry)) + + # ######################################################################################################### + # ######################################################################################################### + # ############# PARAMETERS used in PREPROCESSORS so they need to be updated ############################### + # ######################################################################################################### + # ######################################################################################################### + self.tool = str(tool) + tool_dict = tools[tool]['data'] + # this is the tool diameter, it is used as such to accommodate the preprocessor who need the tool diameter + # given under the name 'toolC' + self.postdata['toolC'] = float(tools[tool]['tooldia']) + self.tooldia = float(tools[tool]['tooldia']) + self.use_ui = True + self.tolerance = tolerance + + # Optimization type. Can be: 'M', 'B', 'T', 'R', 'No' + opt_type = tool_dict['optimization_type'] + opt_time = tool_dict['search_time'] if 'search_time' in tool_dict else 'R' + + if opt_type == 'M': + log.debug("Using OR-Tools Metaheuristic Guided Local Search path optimization.") + elif opt_type == 'B': + log.debug("Using OR-Tools Basic path optimization.") + elif opt_type == 'T': + log.debug("Using Travelling Salesman path optimization.") + elif opt_type == 'R': + log.debug("Using RTree path optimization.") + else: + log.debug("Using no path optimization.") + + # Preprocessor + self.pp_geometry_name = tool_dict['ppname_g'] + self.pp_geometry = self.app.preprocessors[self.pp_geometry_name] + p = self.pp_geometry + + # Offset the Geometry if it is the case + # FIXME need to test if in ["Path", "In", "Out", "Custom"]. For now only 'Custom' is somehow done + offset = tools[tool]['offset_value'] + if offset != 0.0: + for it in flat_geometry: + # if the geometry is a closed shape then create a Polygon out of it + if isinstance(it, LineString): + if it.is_ring: + it = Polygon(it) + temp_solid_geometry.append(it.buffer(offset, join_style=2)) + else: + temp_solid_geometry = flat_geometry + + if self.z_cut is None: + if 'laser' not in self.pp_geometry_name: + self.app.inform.emit( + '[ERROR_NOTCL] %s' % _("Cut_Z parameter is None or zero. Most likely a bad combinations of " + "other parameters.")) + return 'fail' + else: + self.z_cut = 0 + if self.machinist_setting == 0: + if self.z_cut > 0: + self.app.inform.emit('[WARNING] %s' % + _("The Cut Z parameter has positive value. " + "It is the depth value to cut into material.\n" + "The Cut Z parameter needs to have a negative value, assuming it is a typo " + "therefore the app will convert the value to negative." + "Check the resulting CNC code (Gcode etc).")) + self.z_cut = -self.z_cut + elif self.z_cut == 0 and 'laser' not in self.pp_geometry_name: + self.app.inform.emit('[WARNING] %s: %s' % + (_("The Cut Z parameter is zero. There will be no cut, skipping file"), + self.options['name'])) + return 'fail' + + if self.z_move is None: + self.app.inform.emit('[ERROR_NOTCL] %s' % _("Travel Z parameter is None or zero.")) + return 'fail' + + if self.z_move < 0: + self.app.inform.emit('[WARNING] %s' % + _("The Travel Z parameter has negative value. " + "It is the height value to travel between cuts.\n" + "The Z Travel parameter needs to have a positive value, assuming it is a typo " + "therefore the app will convert the value to positive." + "Check the resulting CNC code (Gcode etc).")) + self.z_move = -self.z_move + elif self.z_move == 0: + self.app.inform.emit('[WARNING] %s: %s' % + (_("The Z Travel parameter is zero. This is dangerous, skipping file"), + self.options['name'])) + return 'fail' + + # made sure that depth_per_cut is no more then the z_cut + if abs(self.z_cut) < self.z_depthpercut: + self.z_depthpercut = abs(self.z_cut) + + # Depth parameters + self.z_cut = float(tool_dict['cutz']) + self.multidepth = tool_dict['multidepth'] + self.z_depthpercut = float(tool_dict['depthperpass']) + self.z_move = float(tool_dict['travelz']) + self.f_plunge = self.app.defaults["geometry_f_plunge"] + + self.feedrate = float(tool_dict['feedrate']) + self.z_feedrate = float(tool_dict['feedrate_z']) + self.feedrate_rapid = float(tool_dict['feedrate_rapid']) + + self.spindlespeed = float(tool_dict['spindlespeed']) + self.spindledir = tool_dict['spindledir'] + self.dwell = tool_dict['dwell'] + self.dwelltime = float(tool_dict['dwelltime']) + + self.startz = float(tool_dict['startz']) if tool_dict['startz'] else None + if self.startz == '': + self.startz = None + + self.z_end = float(tool_dict['endz']) + try: + if self.xy_end == '': + self.xy_end = None + else: + # either originally it was a string or not, xy_end will be made string + self.xy_end = re.sub('[()\[\]]', '', str(self.xy_end)) if self.xy_end else None + + # and now, xy_end is made into a list of floats in format [x, y] + if self.xy_end: + self.xy_end = [float(eval(a)) for a in self.xy_end.split(",")] + + if self.xy_end and len(self.xy_end) != 2: + self.app.inform.emit('[ERROR]%s' % _("The End X,Y format has to be (x, y).")) + return 'fail' + except Exception as e: + log.debug("camlib.CNCJob.geometry_from_excellon_by_tool() xy_end --> %s" % str(e)) + self.xy_end = [0, 0] + + self.z_toolchange = tool_dict['toolchangez'] + self.xy_toolchange = tool_dict["toolchangexy"] + try: + if self.xy_toolchange == '': + self.xy_toolchange = None + else: + # either originally it was a string or not, xy_toolchange will be made string + self.xy_toolchange = re.sub('[()\[\]]', '', str(self.xy_toolchange)) if self.xy_toolchange else None + + # and now, xy_toolchange is made into a list of floats in format [x, y] + if self.xy_toolchange: + self.xy_toolchange = [float(eval(a)) for a in self.xy_toolchange.split(",")] + + if self.xy_toolchange and len(self.xy_toolchange) != 2: + self.app.inform.emit('[ERROR] %s' % _("The Toolchange X,Y format has to be (x, y).")) + return 'fail' + except Exception as e: + log.debug("camlib.CNCJob.geometry_from_excellon_by_tool() --> %s" % str(e)) + pass + + self.extracut = tool_dict['extracut'] + self.extracut_length = tool_dict['extracut_length'] + + # Probe parameters + # self.z_pdepth = tool_dict["tools_drill_z_pdepth"] + # self.feedrate_probe = tool_dict["tools_drill_feedrate_probe"] + + # ######################################################################################################### + # ############ Create the data. ########################################################################### + # ######################################################################################################### + optimized_path = [] + + geo_storage = {} + for geo in temp_solid_geometry: + geo_storage[geo.coords[0]] = geo + locations = list(geo_storage.keys()) + + if opt_type == 'M': + # if there are no locations then go to the next tool + if not locations: + return 'fail' + optimized_locations = self.optimized_ortools_meta(locations=locations, opt_time=opt_time) + optimized_path = [(locations[loc], geo_storage[locations[loc]]) for loc in optimized_locations] + elif opt_type == 'B': + # if there are no locations then go to the next tool + if not locations: + return 'fail' + optimized_locations = self.optimized_ortools_basic(locations=locations) + optimized_path = [(locations[loc], geo_storage[locations[loc]]) for loc in optimized_locations] + elif opt_type == 'T': + # if there are no locations then go to the next tool + if not locations: + return 'fail' + optimized_locations = self.optimized_travelling_salesman(locations) + optimized_path = [(loc, geo_storage[loc]) for loc in optimized_locations] + elif opt_type == 'R': + optimized_path = self.geo_optimized_rtree(temp_solid_geometry) + if optimized_path == 'fail': + return 'fail' + else: + # it's actually not optimized path but here we build a list of (x,y) coordinates + # out of the tool's drills + for geo in temp_solid_geometry: + optimized_path.append(geo.coords[0]) + # ######################################################################################################### + # ######################################################################################################### + + # Only if there are locations to drill + if not optimized_path: + log.debug("CNCJob.excellon_tool_gcode_gen() -> Optimized path is empty.") + return 'fail' + + if self.app.abort_flag: + # graceful abort requested by the user + raise grace + + # ############################################################################################################# + # ############################################################################################################# + # ################# MILLING !!! ############################################################################## + # ############################################################################################################# + # ############################################################################################################# + log.debug("Starting G-Code...") + + current_tooldia = float('%.*f' % (self.decimals, float(self.tooldia))) + self.app.inform.emit('%s: %s%s.' % (_("Starting G-Code for tool with diameter"), + str(current_tooldia), + str(self.units))) + + # Measurements + total_travel = 0.0 + total_cut = 0.0 + + # Start GCode + if is_first: + t_gcode += self.doformat(p.start_code) + + # Toolchange code + t_gcode += self.doformat(p.feedrate_code) # sets the feed rate + if toolchange: + t_gcode += self.doformat(p.toolchange_code) + + if 'laser' not in self.pp_geometry_name.lower(): + t_gcode += self.doformat(p.spindle_code) # Spindle start + else: + # for laser this will disable the laser + t_gcode += self.doformat(p.lift_code, x=self.oldx, y=self.oldy) # Move (up) to travel height + + if self.dwell: + t_gcode += self.doformat(p.dwell_code) # Dwell time + else: + t_gcode += self.doformat(p.lift_code, x=0, y=0) # Move (up) to travel height + t_gcode += self.doformat(p.startz_code, x=0, y=0) + + if 'laser' not in self.pp_geometry_name.lower(): + t_gcode += self.doformat(p.spindle_code) # Spindle start + + if self.dwell is True: + t_gcode += self.doformat(p.dwell_code) # Dwell time + t_gcode += self.doformat(p.feedrate_code) # sets the feed rate + + # ## Iterate over geometry paths getting the nearest each time. + path_count = 0 + + # variables to display the percentage of work done + geo_len = len(flat_geometry) + log.warning("Number of paths for which to generate GCode: %s" % str(geo_len)) + old_disp_number = 0 + + current_pt = (0, 0) + for pt, geo in optimized_path: + if self.app.abort_flag: + # graceful abort requested by the user + raise grace + + path_count += 1 + + # If last point in geometry is the nearest but prefer the first one if last point == first point + # then reverse coordinates. + if pt != geo.coords[0] and pt == geo.coords[-1]: + geo.coords = list(geo.coords)[::-1] + + # ---------- Single depth/pass -------- + if not self.multidepth: + # calculate the cut distance + total_cut = total_cut + geo.length + + t_gcode += self.create_gcode_single_pass(geo, current_tooldia, self.extracut, + self.extracut_length, self.tolerance, + z_move=self.z_move, old_point=current_pt) + + # --------- Multi-pass --------- + else: + # calculate the cut distance + # due of the number of cuts (multi depth) it has to multiplied by the number of cuts + nr_cuts = 0 + depth = abs(self.z_cut) + while depth > 0: + nr_cuts += 1 + depth -= float(self.z_depthpercut) + + total_cut += (geo.length * nr_cuts) + + t_gcode += self.create_gcode_multi_pass(geo, current_tooldia, self.extracut, + self.extracut_length, self.tolerance, + z_move=self.z_move, postproc=p, old_point=current_pt) + + # calculate the total distance + total_travel = total_travel + abs(distance(pt1=current_pt, pt2=pt)) + current_pt = geo.coords[-1] + + disp_number = int(np.interp(path_count, [0, geo_len], [0, 100])) + if old_disp_number < disp_number <= 100: + self.app.proc_container.update_view_text(' %d%%' % disp_number) + old_disp_number = disp_number + + log.debug("Finished G-Code... %s paths traced." % path_count) + + # add move to end position + total_travel += abs(distance_euclidian(current_pt[0], current_pt[1], 0, 0)) + self.travel_distance += total_travel + total_cut + self.routing_time += total_cut / self.feedrate + + # Finish + if is_last: + t_gcode += self.doformat(p.spindle_stop_code) + t_gcode += self.doformat(p.lift_code, x=current_pt[0], y=current_pt[1]) + t_gcode += self.doformat(p.end_code, x=0, y=0) + self.app.inform.emit( + '%s... %s %s.' % (_("Finished G-Code generation"), str(path_count), _("paths traced")) + ) + + self.gcode = t_gcode + return self.gcode + def generate_from_geometry_2(self, geometry, append=True, tooldia=None, offset=0.0, tolerance=0, z_cut=None, z_move=None, feedrate=None, feedrate_z=None, feedrate_rapid=None, spindlespeed=None, spindledir='CW', dwell=False, dwelltime=None, multidepth=False, depthpercut=None, @@ -4973,10 +5378,6 @@ class CNCjob(Geometry): :param tool_no: :return: None """ - - if not isinstance(geometry, Geometry): - self.app.inform.emit('[ERROR] %s: %s' % (_("Expected a Geometry, got"), type(geometry))) - return 'fail' log.debug("Executing camlib.CNCJob.generate_from_geometry_2()") # if solid_geometry is empty raise an exception @@ -4984,8 +5385,7 @@ class CNCjob(Geometry): self.app.inform.emit( '[ERROR_NOTCL] %s' % _("Trying to generate a CNC Job from a Geometry object without solid_geometry.") ) - - temp_solid_geometry = [] + return 'fail' def bounds_rec(obj): if type(obj) is list: @@ -5013,6 +5413,8 @@ class CNCjob(Geometry): # it's a Shapely object, return it's bounds return obj.bounds + # Create the solid geometry which will be used to generate GCode + temp_solid_geometry = [] if offset != 0.0: offset_for_use = offset @@ -5110,9 +5512,7 @@ class CNCjob(Geometry): self.xy_toolchange = [float(eval(a)) for a in self.xy_toolchange.split(",")] if len(self.xy_toolchange) < 2: - msg = _("The Toolchange X,Y field in Edit -> Preferences has to be in the format (x, y)\n" - "but now there is only one value, not two.") - self.app.inform.emit('[ERROR] %s' % msg) + self.app.inform.emit('[ERROR] %s' % _("The Toolchange X,Y format has to be (x, y).")) return 'fail' except Exception as e: log.debug("camlib.CNCJob.generate_from_geometry_2() --> %s" % str(e)) diff --git a/defaults.py b/defaults.py index c5fac837..e14fa508 100644 --- a/defaults.py +++ b/defaults.py @@ -298,6 +298,8 @@ class FlatCAMDefaults: "geometry_cnctooldia": "2.4", "geometry_merge_fuse_tools": True, "geometry_plot_line": "#FF0000", + "geometry_optimization_type": 'R', + "geometry_search_time": 3, # Geometry Options "geometry_cutz": -2.4,