- some work in Milling Tool

This commit is contained in:
Marius Stanciu
2020-12-01 18:01:20 +02:00
committed by Marius
parent 4dd38a4bc1
commit edcd75e3a2
2 changed files with 553 additions and 3 deletions

View File

@@ -12,17 +12,20 @@ from appGUI.GUIElements import FCCheckBox, FCDoubleSpinner, RadioSet, FCTable, F
FCComboBox, OptionalInputSection, FCSpinner, NumericalEvalEntry, OptionalHideInputSection, FCLabel
from appParsers.ParseExcellon import Excellon
from camlib import Geometry, grace
from copy import deepcopy
import math
import simplejson as json
import sys
import traceback
from appObjects.FlatCAMObj import FlatCAMObj
# import numpy as np
# import math
# from shapely.ops import unary_union
from shapely.geometry import Point, LineString
from shapely.geometry import Point, LineString, box
from matplotlib.backend_bases import KeyEvent as mpl_key_event
@@ -46,6 +49,7 @@ else:
class ToolMilling(AppTool, Excellon):
builduiSig = QtCore.pyqtSignal()
launch_job = QtCore.pyqtSignal()
def __init__(self, app):
self.app = app
@@ -227,7 +231,7 @@ class ToolMilling(AppTool, Excellon):
self.ui.apply_param_to_all.clicked.connect(self.on_apply_param_to_all_clicked)
self.ui.generate_cnc_button.clicked.connect(self.on_cnc_button_click)
self.ui.tools_table.drag_drop_sig.connect(self.rebuild_ui)
self.ui.tools_table.drag_drop_sig.connect(self.on_exc_rebuild_ui)
# Exclusion areas signals
self.ui.exclusion_table.horizontalHeader().sectionClicked.connect(self.exclusion_table_toggle_all)
@@ -238,6 +242,12 @@ class ToolMilling(AppTool, Excellon):
self.ui.delete_sel_area_button.clicked.connect(self.on_delete_sel_areas)
self.ui.strategy_radio.activated_custom.connect(self.on_strategy)
# Geo Tools Table signals
self.ui.geo_tools_table.drag_drop_sig.connect(self.on_geo_rebuild_ui)
self.ui.geo_tools_table.horizontalHeader().sectionClicked.connect(self.on_toggle_all_rows)
self.launch_job.connect(self.mtool_gen_cncjob)
self.ui.reset_button.clicked.connect(self.set_tool_ui)
# Cleanup on Graceful exit (CTRL+ALT+X combo key)
self.app.cleanup.connect(self.set_tool_ui)
@@ -452,7 +462,7 @@ class ToolMilling(AppTool, Excellon):
def on_plot_clicked(self, state):
self.target_obj.options['plot'] = True if state else False
def rebuild_ui(self):
def on_exc_rebuild_ui(self):
# read the table tools uid
current_uid_list = []
for row in range(self.ui.tools_table.rowCount()):
@@ -469,6 +479,26 @@ class ToolMilling(AppTool, Excellon):
# the tools table changed therefore we need to rebuild it
QtCore.QTimer.singleShot(20, self.build_ui)
def on_geo_rebuild_ui(self):
# read the table tools uid
current_uid_list = []
for row in range(self.ui.geo_tools_table.rowCount()):
uid = int(self.ui.geo_tools_table.item(row, 3).text())
current_uid_list.append(uid)
new_tools = {}
new_uid = 1
for current_uid in current_uid_list:
new_tools[new_uid] = deepcopy(self.tools[current_uid])
new_uid += 1
self.tools = new_tools
# the tools table changed therefore we need to reconnect the signals to the cellWidgets
self.ui_disconnect()
self.ui_connect()
def build_ui(self):
self.ui_disconnect()
@@ -2170,6 +2200,525 @@ class ToolMilling(AppTool, Excellon):
return True, ""
def on_polish(self):
def job_thread(obj):
with obj.app.proc_container.new('%s...' % _("Working")):
tooldia = obj.ui.polish_dia_entry.get_value()
depth = obj.ui.polish_pressure_entry.get_value()
travelz = obj.ui.polish_travelz_entry.get_value()
margin = obj.ui.polish_margin_entry.get_value()
overlap = obj.ui.polish_over_entry.get_value() / 100
paint_method = obj.ui.polish_method_combo.get_value()
# calculate the max uid form the keys of the self.tools
max_uid = max(list(obj.tools.keys()))
new_uid = max_uid + 1
# add a new key in the dict
new_data = deepcopy(obj.default_data)
new_data["travelz"] = travelz
new_data["cutz"] = depth
new_dict = {
new_uid: {
'tooldia': obj.app.dec_format(float(tooldia), obj.decimals),
'offset': 'Path',
'offset_value': 0.0,
'type': _('Polish'),
'tool_type': 'C1',
'data': new_data,
'solid_geometry': []
}
}
obj.tools.update(new_dict)
obj.sel_tools.update(new_dict)
# make a box polygon out of the bounds of the current object
# apply the margin
xmin, ymin, xmax, ymax = obj.bounds()
bbox = box(xmin-margin, ymin-margin, xmax+margin, ymax+margin)
# paint the box
try:
# provide the app with a way to process the GUI events when in a blocking loop
QtWidgets.QApplication.processEvents()
if self.app.abort_flag:
# graceful abort requested by the user
raise grace
# Type(cpoly) == FlatCAMRTreeStorage | None
cpoly = None
if paint_method == 0: # Standard
cpoly = self.clear_polygon(bbox,
tooldia=tooldia,
steps_per_circle=obj.circle_steps,
overlap=overlap,
contour=True,
connect=True,
prog_plot=False)
elif paint_method == 1: # Seed
cpoly = self.clear_polygon2(bbox,
tooldia=tooldia,
steps_per_circle=obj.circle_steps,
overlap=overlap,
contour=True,
connect=True,
prog_plot=False)
elif paint_method == 2: # Lines
cpoly = self.clear_polygon3(bbox,
tooldia=tooldia,
steps_per_circle=obj.circle_steps,
overlap=overlap,
contour=True,
connect=True,
prog_plot=False)
if not cpoly or not cpoly.objects:
obj.app.inform.emit('[ERROR_NOTCL] %s' % _('Geometry could not be painted completely'))
return
paint_geo = [g for g in cpoly.get_objects() if g and not g.is_empty]
except grace:
return "fail"
except Exception as e:
log.debug("Could not Paint the polygons. %s" % str(e))
mssg = '[ERROR] %s\n%s' % (_("Could not do Paint. Try a different combination of parameters. "
"Or a different method of Paint"), str(e))
self.app.inform.emit(mssg)
return
obj.sel_tools[new_uid]['solid_geometry'] = paint_geo
# and now create the CNCJob
obj.launch_job.emit()
# Send to worker
self.app.worker_task.emit({'fcn': job_thread, 'params': [self]})
def on_generatecnc_button_click(self):
log.debug("Generating CNCJob from Geometry ...")
self.app.defaults.report_usage("geometry_on_generatecnc_button")
self.sel_tools = {}
try:
if self.special_group:
self.app.inform.emit(
'[WARNING_NOTCL] %s %s %s.' %
(_("This Geometry can't be processed because it is"), str(self.special_group), _("Geometry"))
)
return
except AttributeError:
pass
# 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():
tooluid = int(self.ui.geo_tools_table.item(x.row(), 3).text())
for tooluid_key, tooluid_value in self.tools.items():
if int(tooluid_key) == tooluid:
self.sel_tools.update({
tooluid: deepcopy(tooluid_value)
})
if self.ui.polish_cb.get_value():
self.on_polish()
else:
self.mtool_gen_cncjob()
self.ui.geo_tools_table.clearSelection()
elif self.ui.geo_tools_table.rowCount() == 1:
tooluid = int(self.ui.geo_tools_table.item(0, 3).text())
for tooluid_key, tooluid_value in self.tools.items():
if int(tooluid_key) == tooluid:
self.sel_tools.update({
tooluid: deepcopy(tooluid_value)
})
if self.ui.polish_cb.get_value():
self.on_polish()
else:
self.mtool_gen_cncjob()
self.ui.geo_tools_table.clearSelection()
else:
self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed. No tool selected in the tool table ..."))
def mtool_gen_cncjob(self, outname=None, tools_dict=None, tools_in_use=None, segx=None, segy=None,
plot=True, use_thread=True):
"""
Creates a multi-tool CNCJob out of this Geometry object.
The actual work is done by the target CNCJobObject object's
`generate_from_geometry_2()` method.
:param outname:
:param tools_dict: a dictionary that holds the whole data needed to create the Gcode
(including the solid_geometry)
:param tools_in_use: the tools that are used, needed by some preprocessors
:type tools_in_use list of lists, each list in the list is made out of row elements of tools table from GUI
:param segx: number of segments on the X axis, for auto-levelling
:param segy: number of segments on the Y axis, for auto-levelling
:param plot: if True the generated object will be plotted; if False will not be plotted
:param use_thread: if True use threading
:return: None
"""
# use the name of the first tool selected in self.geo_tools_table which has the diameter passed as tool_dia
outname = "%s_%s" % (self.options["name"], 'cnc') if outname is None else outname
tools_dict = self.sel_tools if tools_dict is None else tools_dict
tools_in_use = tools_in_use if tools_in_use is not None else self.get_selected_tools_table_items()
segx = segx if segx is not None else float(self.app.defaults['geometry_segx'])
segy = segy if segy is not None else float(self.app.defaults['geometry_segy'])
try:
xmin = self.options['xmin']
ymin = self.options['ymin']
xmax = self.options['xmax']
ymax = self.options['ymax']
except Exception as e:
log.debug("FlatCAMObj.GeometryObject.mtool_gen_cncjob() --> %s\n" % str(e))
msg = '[ERROR] %s' % _("An internal error has occurred. See shell.\n")
msg += '%s' % str(e)
msg += traceback.format_exc()
self.app.inform.emit(msg)
return
# force everything as MULTI-GEO
# 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):
log.debug("Creating a CNCJob out of a single-geometry")
assert job_obj.kind == 'cncjob', "Initializer expected a CNCJobObject, got %s" % type(job_obj)
job_obj.options['xmin'] = xmin
job_obj.options['ymin'] = ymin
job_obj.options['xmax'] = xmax
job_obj.options['ymax'] = ymax
# count the tools
tool_cnt = 0
# dia_cnc_dict = {}
# this turn on the FlatCAMCNCJob plot for multiple tools
job_obj.multitool = True
job_obj.multigeo = False
job_obj.cnc_tools.clear()
job_obj.options['Tools_in_use'] = tools_in_use
job_obj.segx = segx if segx else float(self.app.defaults["geometry_segx"])
job_obj.segy = segy if segy else float(self.app.defaults["geometry_segy"])
job_obj.z_pdepth = float(self.app.defaults["geometry_z_pdepth"])
job_obj.feedrate_probe = float(self.app.defaults["geometry_feedrate_probe"])
total_gcode = ''
for tooluid_key in list(tools_dict.keys()):
tool_cnt += 1
dia_cnc_dict = deepcopy(tools_dict[tooluid_key])
tooldia_val = app_obj.dec_format(float(tools_dict[tooluid_key]['tooldia']), self.decimals)
dia_cnc_dict.update({
'tooldia': tooldia_val
})
if dia_cnc_dict['offset'] == 'in':
tool_offset = -dia_cnc_dict['tooldia'] / 2
elif dia_cnc_dict['offset'].lower() == 'out':
tool_offset = dia_cnc_dict['tooldia'] / 2
elif dia_cnc_dict['offset'].lower() == 'custom':
try:
offset_value = float(self.ui.tool_offset_entry.get_value())
except ValueError:
# try to convert comma to decimal point. if it's still not working error message and return
try:
offset_value = float(self.ui.tool_offset_entry.get_value().replace(',', '.'))
except ValueError:
app_obj.inform.emit('[ERROR_NOTCL] %s' % _("Wrong value format entered, use a number."))
return
if offset_value:
tool_offset = float(offset_value)
else:
app_obj.inform.emit(
'[WARNING] %s' % _("Tool Offset is selected in Tool Table but no value is provided.\n"
"Add a Tool Offset or change the Offset Type.")
)
return
else:
tool_offset = 0.0
dia_cnc_dict.update({
'offset_value': tool_offset
})
z_cut = tools_dict[tooluid_key]['data']["cutz"]
z_move = tools_dict[tooluid_key]['data']["travelz"]
feedrate = tools_dict[tooluid_key]['data']["feedrate"]
feedrate_z = tools_dict[tooluid_key]['data']["feedrate_z"]
feedrate_rapid = tools_dict[tooluid_key]['data']["feedrate_rapid"]
multidepth = tools_dict[tooluid_key]['data']["multidepth"]
extracut = tools_dict[tooluid_key]['data']["extracut"]
extracut_length = tools_dict[tooluid_key]['data']["extracut_length"]
depthpercut = tools_dict[tooluid_key]['data']["depthperpass"]
toolchange = tools_dict[tooluid_key]['data']["toolchange"]
toolchangez = tools_dict[tooluid_key]['data']["toolchangez"]
toolchangexy = tools_dict[tooluid_key]['data']["toolchangexy"]
startz = tools_dict[tooluid_key]['data']["startz"]
endz = tools_dict[tooluid_key]['data']["endz"]
endxy = self.options["endxy"]
spindlespeed = tools_dict[tooluid_key]['data']["spindlespeed"]
dwell = tools_dict[tooluid_key]['data']["dwell"]
dwelltime = tools_dict[tooluid_key]['data']["dwelltime"]
pp_geometry_name = tools_dict[tooluid_key]['data']["ppname_g"]
spindledir = self.app.defaults['geometry_spindledir']
tool_solid_geometry = self.solid_geometry
job_obj.coords_decimals = self.app.defaults["cncjob_coords_decimals"]
job_obj.fr_decimals = self.app.defaults["cncjob_fr_decimals"]
# Propagate options
job_obj.options["tooldia"] = tooldia_val
job_obj.options['type'] = 'Geometry'
job_obj.options['tool_dia'] = tooldia_val
tool_lst = list(tools_dict.keys())
is_first = True if tooluid_key == tool_lst[0] else False
# 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, start_gcode = job_obj.generate_from_geometry_2(
self, 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, is_first=is_first)
if res == 'fail':
log.debug("GeometryObject.mtool_gen_cncjob() --> generate_from_geometry2() failed")
return 'fail'
dia_cnc_dict['gcode'] = res
if start_gcode != '':
job_obj.gc_start = start_gcode
total_gcode += res
# tell gcode_parse from which point to start drawing the lines depending on what kind of
# object is the source of gcode
job_obj.toolchange_xy_type = "geometry"
self.app.inform.emit('[success] %s' % _("G-Code parsing in progress..."))
dia_cnc_dict['gcode_parsed'] = job_obj.gcode_parse()
app_obj.inform.emit('[success] %s' % _("G-Code parsing finished..."))
# commented this; there is no need for the actual GCode geometry - the original one will serve as well
# for bounding box values
# dia_cnc_dict['solid_geometry'] = unary_union([geo['geom'] for geo in dia_cnc_dict['gcode_parsed']])
try:
dia_cnc_dict['solid_geometry'] = tool_solid_geometry
app_obj.inform.emit('[success] %s...' % _("Finished G-Code processing"))
except Exception as er:
app_obj.inform.emit('[ERROR] %s: %s' % (_("G-Code processing failed with error"), str(er)))
job_obj.cnc_tools.update({
tooluid_key: deepcopy(dia_cnc_dict)
})
dia_cnc_dict.clear()
job_obj.source_file = job_obj.gc_start + total_gcode
# Object initialization function for app.app_obj.new_object()
# RUNNING ON SEPARATE THREAD!
def job_init_multi_geometry(job_obj, app_obj):
log.debug("Creating a CNCJob out of a multi-geometry")
assert job_obj.kind == 'cncjob', "Initializer expected a CNCJobObject, got %s" % type(job_obj)
job_obj.options['xmin'] = xmin
job_obj.options['ymin'] = ymin
job_obj.options['xmax'] = xmax
job_obj.options['ymax'] = ymax
# count the tools
tool_cnt = 0
# dia_cnc_dict = {}
# this turn on the FlatCAMCNCJob plot for multiple tools
job_obj.multitool = True
job_obj.multigeo = True
job_obj.cnc_tools.clear()
job_obj.options['Tools_in_use'] = tools_in_use
job_obj.segx = segx if segx else float(self.app.defaults["geometry_segx"])
job_obj.segy = segy if segy else float(self.app.defaults["geometry_segy"])
job_obj.z_pdepth = float(self.app.defaults["geometry_z_pdepth"])
job_obj.feedrate_probe = float(self.app.defaults["geometry_feedrate_probe"])
# make sure that trying to make a CNCJob from an empty file is not creating an app crash
if not self.solid_geometry:
a = 0
for tooluid_key in self.tools:
if self.tools[tooluid_key]['solid_geometry'] is None:
a += 1
if a == len(self.tools):
app_obj.inform.emit('[ERROR_NOTCL] %s...' % _('Cancelled. Empty file, it has no geometry'))
return 'fail'
total_gcode = ''
for tooluid_key in list(tools_dict.keys()):
tool_cnt += 1
dia_cnc_dict = deepcopy(tools_dict[tooluid_key])
tooldia_val = app_obj.dec_format(float(tools_dict[tooluid_key]['tooldia']), self.decimals)
dia_cnc_dict.update({
'tooldia': tooldia_val
})
if "optimization_type" not in tools_dict[tooluid_key]['data']:
tools_dict[tooluid_key]['data']["optimization_type"] = \
self.app.defaults["geometry_optimization_type"]
# find the tool_dia associated with the tooluid_key
# search in the self.tools for the sel_tool_dia and when found see what tooluid has
# on the found tooluid in self.tools we also have the solid_geometry that interest us
# for k, v in self.tools.items():
# if float('%.*f' % (self.decimals, float(v['tooldia']))) == tooldia_val:
# current_uid = int(k)
# break
if dia_cnc_dict['offset'].lower() == 'in':
tool_offset = -tooldia_val / 2
elif dia_cnc_dict['offset'].lower() == 'out':
tool_offset = tooldia_val / 2
elif dia_cnc_dict['offset'].lower() == 'custom':
offset_value = float(self.ui.tool_offset_entry.get_value())
if offset_value:
tool_offset = float(offset_value)
else:
self.app.inform.emit('[WARNING] %s' %
_("Tool Offset is selected in Tool Table but "
"no value is provided.\n"
"Add a Tool Offset or change the Offset Type."))
return
else:
tool_offset = 0.0
dia_cnc_dict.update({
'offset_value': tool_offset
})
# z_cut = tools_dict[tooluid_key]['data']["cutz"]
# z_move = tools_dict[tooluid_key]['data']["travelz"]
# feedrate = tools_dict[tooluid_key]['data']["feedrate"]
# feedrate_z = tools_dict[tooluid_key]['data']["feedrate_z"]
# feedrate_rapid = tools_dict[tooluid_key]['data']["feedrate_rapid"]
# multidepth = tools_dict[tooluid_key]['data']["multidepth"]
# extracut = tools_dict[tooluid_key]['data']["extracut"]
# extracut_length = tools_dict[tooluid_key]['data']["extracut_length"]
# depthpercut = tools_dict[tooluid_key]['data']["depthperpass"]
# toolchange = tools_dict[tooluid_key]['data']["toolchange"]
# toolchangez = tools_dict[tooluid_key]['data']["toolchangez"]
# toolchangexy = tools_dict[tooluid_key]['data']["toolchangexy"]
# startz = tools_dict[tooluid_key]['data']["startz"]
# endz = tools_dict[tooluid_key]['data']["endz"]
# endxy = self.options["endxy"]
# spindlespeed = tools_dict[tooluid_key]['data']["spindlespeed"]
# dwell = tools_dict[tooluid_key]['data']["dwell"]
# dwelltime = tools_dict[tooluid_key]['data']["dwelltime"]
# pp_geometry_name = tools_dict[tooluid_key]['data']["ppname_g"]
#
# spindledir = self.app.defaults['geometry_spindledir']
tool_solid_geometry = self.tools[tooluid_key]['solid_geometry']
job_obj.coords_decimals = self.app.defaults["cncjob_coords_decimals"]
job_obj.fr_decimals = self.app.defaults["cncjob_fr_decimals"]
# Propagate options
job_obj.options["tooldia"] = tooldia_val
job_obj.options['type'] = 'Geometry'
job_obj.options['tool_dia'] = tooldia_val
# 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
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, start_gcode = 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'
else:
dia_cnc_dict['gcode'] = res
total_gcode += res
if start_gcode != '':
job_obj.gc_start = start_gcode
app_obj.inform.emit('[success] %s' % _("G-Code parsing in progress..."))
dia_cnc_dict['gcode_parsed'] = job_obj.gcode_parse()
app_obj.inform.emit('[success] %s' % _("G-Code parsing finished..."))
# commented this; there is no need for the actual GCode geometry - the original one will serve as well
# for bounding box values
# geo_for_bound_values = unary_union([
# geo['geom'] for geo in dia_cnc_dict['gcode_parsed'] if geo['geom'].is_valid is True
# ])
try:
dia_cnc_dict['solid_geometry'] = deepcopy(tool_solid_geometry)
app_obj.inform.emit('[success] %s...' % _("Finished G-Code processing"))
except Exception as ee:
app_obj.inform.emit('[ERROR] %s: %s' % (_("G-Code processing failed with error"), str(ee)))
# tell gcode_parse from which point to start drawing the lines depending on what kind of
# object is the source of gcode
job_obj.toolchange_xy_type = "geometry"
job_obj.cnc_tools.update({
tooluid_key: deepcopy(dia_cnc_dict)
})
dia_cnc_dict.clear()
job_obj.source_file = total_gcode
if use_thread:
# To be run in separate thread
def job_thread(a_obj):
if self.multigeo is False:
with self.app.proc_container.new('%s...' % _("Generating")):
ret_val = a_obj.app_obj.new_object("cncjob", outname, job_init_single_geometry, plot=plot)
if ret_val != 'fail':
a_obj.inform.emit('[success] %s: %s' % (_("CNCjob created"), outname))
else:
with self.app.proc_container.new('%s...' % _("Generating")):
ret_val = a_obj.app_obj.new_object("cncjob", outname, job_init_multi_geometry, plot=plot)
if ret_val != 'fail':
a_obj.inform.emit('[success] %s: %s' % (_("CNCjob created"), outname))
# Create a promise with the name
self.app.collection.promise(outname)
# Send to worker
self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
else:
if self.solid_geometry:
self.app.app_obj.new_object("cncjob", outname, job_init_single_geometry, plot=plot)
else:
self.app.app_obj.new_object("cncjob", outname, job_init_multi_geometry, plot=plot)
def on_pp_changed(self):
current_pp = self.ui.pp_geo_name_cb.get_value()