- 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
This commit is contained in:
Marius Stanciu
2020-07-16 04:55:58 +03:00
parent 6c3774be7a
commit 144a89f686
8 changed files with 601 additions and 166 deletions

View File

@@ -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 15.07.2020
- added icons to some of the push buttons - added icons to some of the push buttons

View File

@@ -246,6 +246,8 @@ class PreferencesUIManager:
"geometry_cnctooldia": self.ui.geometry_defaults_form.geometry_gen_group.cnctooldia_entry, "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_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_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 Options
"geometry_cutz": self.ui.geometry_defaults_form.geometry_opt_group.cutz_entry, "geometry_cutz": self.ui.geometry_defaults_form.geometry_opt_group.cutz_entry,

View File

@@ -207,7 +207,7 @@ class ExcellonGenPrefGroupUI(OptionsGroupUI):
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
grid2.addWidget(separator_line, 7, 0, 1, 2) grid2.addWidget(separator_line, 7, 0, 1, 2)
self.excellon_general_label = QtWidgets.QLabel("<b>%s:</b>" % _("Excellon Optimization")) self.excellon_general_label = QtWidgets.QLabel("<b>%s:</b>" % _("Path Optimization"))
grid2.addWidget(self.excellon_general_label, 8, 0, 1, 2) grid2.addWidget(self.excellon_general_label, 8, 0, 1, 2)
self.excellon_optimization_label = QtWidgets.QLabel(_('Algorithm:')) self.excellon_optimization_label = QtWidgets.QLabel(_('Algorithm:'))

View File

@@ -1,7 +1,7 @@
from PyQt5 import QtWidgets from PyQt5 import QtWidgets
from PyQt5.QtCore import QSettings 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 from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
import gettext import gettext
@@ -86,25 +86,72 @@ class GeometryGenPrefGroupUI(OptionsGroupUI):
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
grid0.addWidget(separator_line, 9, 0, 1, 2) grid0.addWidget(separator_line, 9, 0, 1, 2)
self.opt_label = QtWidgets.QLabel("<b>%s:</b>" % _("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 # Fuse Tools
self.join_geo_label = QtWidgets.QLabel('<b>%s</b>:' % _('Join Option')) self.join_geo_label = QtWidgets.QLabel('<b>%s</b>:' % _('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 = FCCheckBox(_("Fuse Tools"))
self.fuse_tools_cb.setToolTip( self.fuse_tools_cb.setToolTip(
_("When checked the joined (merged) object tools\n" _("When checked the joined (merged) object tools\n"
"will be merged also but only if they share some of their attributes.") "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 = QtWidgets.QFrame()
separator_line.setFrameShape(QtWidgets.QFrame.HLine) separator_line.setFrameShape(QtWidgets.QFrame.HLine)
separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) 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 # Geometry Object Color
self.gerber_color_label = QtWidgets.QLabel('<b>%s</b>:' % _('Object Color')) self.gerber_color_label = QtWidgets.QLabel('<b>%s</b>:' % _('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 # Plot Line Color
self.line_color_label = QtWidgets.QLabel('%s:' % _('Outline')) self.line_color_label = QtWidgets.QLabel('%s:' % _('Outline'))
@@ -113,8 +160,8 @@ class GeometryGenPrefGroupUI(OptionsGroupUI):
) )
self.line_color_entry = FCColorEntry() self.line_color_entry = FCColorEntry()
grid0.addWidget(self.line_color_label, 14, 0) grid0.addWidget(self.line_color_label, 26, 0)
grid0.addWidget(self.line_color_entry, 14, 1) grid0.addWidget(self.line_color_entry, 26, 1)
self.layout.addStretch() self.layout.addStretch()

View File

@@ -474,41 +474,19 @@ class GeometryObject(FlatCAMObj, Geometry):
# store here the default data for Geometry Data # store here the default data for Geometry Data
self.default_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 # fill in self.default_data values from self.options
for def_key in self.default_data: # for def_key in self.default_data:
for opt_key, opt_val in self.options.items(): # for opt_key, opt_val in self.options.items():
if def_key == opt_key: # if def_key == opt_key:
self.default_data[def_key] = deepcopy(opt_val) # self.default_data[def_key] = deepcopy(opt_val)
if type(self.options["cnctooldia"]) == float: if type(self.options["cnctooldia"]) == float:
tools_list = [self.options["cnctooldia"]] 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 # test to see if we have tools available in the tool table
if self.ui.geo_tools_table.selectedItems(): if self.ui.geo_tools_table.selectedItems():
for x in 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()) tooluid = int(self.ui.geo_tools_table.item(x.row(), 5).text())
for tooluid_key, tooluid_value in self.tools.items(): for tooluid_key, tooluid_value in self.tools.items():
@@ -1884,6 +1852,7 @@ class GeometryObject(FlatCAMObj, Geometry):
self.app.inform.emit(msg) self.app.inform.emit(msg)
return return
self.multigeo = True
# Object initialization function for app.app_obj.new_object() # Object initialization function for app.app_obj.new_object()
# RUNNING ON SEPARATE THREAD! # RUNNING ON SEPARATE THREAD!
def job_init_single_geometry(job_obj, app_obj): 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 # 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 # to a value of 0.0005 which is 20 times less than 0.01
tol = float(self.app.defaults['global_tolerance']) / 20 tol = float(self.app.defaults['global_tolerance']) / 20
res = job_obj.generate_from_multitool_geometry( # res = job_obj.generate_from_multitool_geometry(
tool_solid_geometry, tooldia=tooldia_val, offset=tool_offset, # tool_solid_geometry, tooldia=tooldia_val, offset=tool_offset,
tolerance=tol, z_cut=z_cut, z_move=z_move, # tolerance=tol, z_cut=z_cut, z_move=z_move,
feedrate=feedrate, feedrate_z=feedrate_z, feedrate_rapid=feedrate_rapid, # feedrate=feedrate, feedrate_z=feedrate_z, feedrate_rapid=feedrate_rapid,
spindlespeed=spindlespeed, spindledir=spindledir, dwell=dwell, dwelltime=dwelltime, # spindlespeed=spindlespeed, spindledir=spindledir, dwell=dwell, dwelltime=dwelltime,
multidepth=multidepth, depthpercut=depthpercut, # multidepth=multidepth, depthpercut=depthpercut,
extracut=extracut, extracut_length=extracut_length, startz=startz, endz=endz, endxy=endxy, # extracut=extracut, extracut_length=extracut_length, startz=startz, endz=endz, endxy=endxy,
toolchange=toolchange, toolchangez=toolchangez, toolchangexy=toolchangexy, # toolchange=toolchange, toolchangez=toolchangez, toolchangexy=toolchangexy,
pp_geometry_name=pp_geometry_name, # pp_geometry_name=pp_geometry_name,
tool_no=tool_cnt) # 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': if res == 'fail':
log.debug("GeometryObject.mtool_gen_cncjob() --> generate_from_geometry2() failed") log.debug("GeometryObject.mtool_gen_cncjob() --> generate_from_geometry2() failed")
return 'fail' return 'fail'

View File

@@ -345,8 +345,7 @@ class ToolIsolation(AppTool, Gerber):
"feedrate": self.app.defaults["geometry_feedrate"], "feedrate": self.app.defaults["geometry_feedrate"],
"feedrate_z": self.app.defaults["geometry_feedrate_z"], "feedrate_z": self.app.defaults["geometry_feedrate_z"],
"feedrate_rapid": self.app.defaults["geometry_feedrate_rapid"], "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"], "multidepth": self.app.defaults["geometry_multidepth"],
"ppname_g": self.app.defaults["geometry_ppname_g"], "ppname_g": self.app.defaults["geometry_ppname_g"],
"depthperpass": self.app.defaults["geometry_depthperpass"], "depthperpass": self.app.defaults["geometry_depthperpass"],
@@ -357,7 +356,13 @@ class ToolIsolation(AppTool, Gerber):
"endz": self.app.defaults["geometry_endz"], "endz": self.app.defaults["geometry_endz"],
"endxy": self.app.defaults["geometry_endxy"], "endxy": self.app.defaults["geometry_endxy"],
"dwell": self.app.defaults["geometry_dwell"],
"dwelltime": self.app.defaults["geometry_dwelltime"],
"spindlespeed": self.app.defaults["geometry_spindlespeed"], "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"], "toolchangexy": self.app.defaults["geometry_toolchangexy"],
"startz": self.app.defaults["geometry_startz"], "startz": self.app.defaults["geometry_startz"],

604
camlib.py
View File

@@ -2518,8 +2518,11 @@ class CNCjob(Geometry):
self.z_end = endz self.z_end = endz
self.xy_end = endxy self.xy_end = endxy
self.extracut = False
self.extracut_length = None self.extracut_length = None
self.tolerance = self.drawing_tolerance
# used by the self.generate_from_excellon_by_tool() method # 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 # but set directly before the actual usage of the method with obj.excellon_optimization_type = value
self.excellon_optimization_type = 'No' self.excellon_optimization_type = 'No'
@@ -2721,7 +2724,7 @@ class CNCjob(Geometry):
# Create the data. # Create the data.
return [(pt.coords.xy[0][0], pt.coords.xy[1][0]) for pt in points] 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 = [] optimized_path = []
tsp_size = len(locations) tsp_size = len(locations)
@@ -2731,56 +2734,57 @@ class CNCjob(Geometry):
depot = 0 if start is None else start depot = 0 if start is None else start
# Create routing model. # Create routing model.
if tsp_size > 0: 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:
log.warning('OR-tools metaheuristics - Specify an instance greater than 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 return optimized_path
# ############################################# ## # ############################################# ##
@@ -2795,43 +2799,44 @@ class CNCjob(Geometry):
depot = 0 if start is None else start depot = 0 if start is None else start
# Create routing model. # Create routing model.
if tsp_size > 0: 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:
log.warning('Specify an instance greater than 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 return optimized_path
# ############################################# ## # ############################################# ##
@@ -2871,6 +2876,46 @@ class CNCjob(Geometry):
must_visit.remove(nearest) must_visit.remove(nearest)
return path 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): def check_zcut(self, zcut):
if zcut > 0: if zcut > 0:
self.app.inform.emit('[WARNING] %s' % 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] # and now, xy_toolchange is made into a list of floats in format [x, y]
if self.xy_toolchange: if self.xy_toolchange:
self.xy_toolchange = [ self.xy_toolchange = [float(eval(a)) for a in self.xy_toolchange.split(",")]
float(eval(a)) for a in self.xy_toolchange.split(",")
]
if self.xy_toolchange and len(self.xy_toolchange) != 2: 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' return 'fail'
except Exception as e: except Exception as e:
log.debug("camlib.CNCJob.generate_from_excellon_by_tool() xy_toolchange --> %s" % str(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 there are no locations then go to the next tool
if not locations: if not locations:
return 'fail' 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': elif opt_type == 'B':
locations = self.create_tool_data_array(points=points) locations = self.create_tool_data_array(points=points)
# if there are no locations then go to the next tool # 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 there are no locations then go to the next tool
if not locations: if not locations:
continue 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': elif used_excellon_optimization_type == 'B':
if tool in points: if tool in points:
locations = self.create_tool_data_array(points=points[tool]) 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 there are no locations then go to the next tool
if not locations: if not locations:
return 'fail' 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': elif used_excellon_optimization_type == 'B':
if all_points: if all_points:
locations = self.create_tool_data_array(points=all_points) locations = self.create_tool_data_array(points=all_points)
@@ -4931,6 +4977,365 @@ class CNCjob(Geometry):
) )
return self.gcode 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, 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, z_move=None, feedrate=None, feedrate_z=None, feedrate_rapid=None, spindlespeed=None,
spindledir='CW', dwell=False, dwelltime=None, multidepth=False, depthpercut=None, spindledir='CW', dwell=False, dwelltime=None, multidepth=False, depthpercut=None,
@@ -4973,10 +5378,6 @@ class CNCjob(Geometry):
:param tool_no: :param tool_no:
:return: None :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()") log.debug("Executing camlib.CNCJob.generate_from_geometry_2()")
# if solid_geometry is empty raise an exception # if solid_geometry is empty raise an exception
@@ -4984,8 +5385,7 @@ class CNCjob(Geometry):
self.app.inform.emit( self.app.inform.emit(
'[ERROR_NOTCL] %s' % _("Trying to generate a CNC Job from a Geometry object without solid_geometry.") '[ERROR_NOTCL] %s' % _("Trying to generate a CNC Job from a Geometry object without solid_geometry.")
) )
return 'fail'
temp_solid_geometry = []
def bounds_rec(obj): def bounds_rec(obj):
if type(obj) is list: if type(obj) is list:
@@ -5013,6 +5413,8 @@ class CNCjob(Geometry):
# it's a Shapely object, return it's bounds # it's a Shapely object, return it's bounds
return obj.bounds return obj.bounds
# Create the solid geometry which will be used to generate GCode
temp_solid_geometry = []
if offset != 0.0: if offset != 0.0:
offset_for_use = offset offset_for_use = offset
@@ -5110,9 +5512,7 @@ class CNCjob(Geometry):
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 len(self.xy_toolchange) < 2: if len(self.xy_toolchange) < 2:
msg = _("The Toolchange X,Y field in Edit -> Preferences has to be in the format (x, y)\n" self.app.inform.emit('[ERROR] %s' % _("The Toolchange X,Y format has to be (x, y)."))
"but now there is only one value, not two.")
self.app.inform.emit('[ERROR] %s' % msg)
return 'fail' return 'fail'
except Exception as e: except Exception as e:
log.debug("camlib.CNCJob.generate_from_geometry_2() --> %s" % str(e)) log.debug("camlib.CNCJob.generate_from_geometry_2() --> %s" % str(e))

View File

@@ -298,6 +298,8 @@ class FlatCAMDefaults:
"geometry_cnctooldia": "2.4", "geometry_cnctooldia": "2.4",
"geometry_merge_fuse_tools": True, "geometry_merge_fuse_tools": True,
"geometry_plot_line": "#FF0000", "geometry_plot_line": "#FF0000",
"geometry_optimization_type": 'R',
"geometry_search_time": 3,
# Geometry Options # Geometry Options
"geometry_cutz": -2.4, "geometry_cutz": -2.4,